编程语言基础:Agda 描述

Philip Wadler

Wen Kokke

Jeremy G. Siek

2024-03

Creative Commons Attribution 4.0 International License

前言

Dedication: 题献

Philip 致 Wanda

我生命中的至爱

咚 咚 咚

Preface: 前言

逻辑与计算之间最深刻的联系是一种双关。「命题即类型(Propositions as Types)」 的学说断言,形式化的结构可以按两种方式看待:可以看做逻辑中的命题, 也可以看做计算中的类型。此外,相关的结构可以看做命题的证明或者其相应类型的程序。 更进一步来说,证明的化简与程序的求值对应。

与此相应,本书的名字也有两种含义。它可以看做「编程语言的基础」,也可以看做 「编程的语言基础」。我们用 Agda 证明助理编写的规范(Specification) 同时描述了编程语言以及该语言编写的程序自身。

本书面向本科最后一年的学生、一年级的研究生或博士生。 本书以简单类型 λ-演算(Simply-Typed Lambda Calculus,简称 STLC)作为核心示例, 旨在教授编程语言的操作语义基础。全书以 Agda 文学脚本(Literal Script)的形式写成。 使用证明助理可以让开发过程变得更加具体而清晰易懂,还可以给予学生即时反馈, 帮助学生发现理解有误的地方并及时纠正。

【译注:文学编程(Literal Programming)用带有代码的自然语言文章来进行编程, 此概念由高德纳(Donald Knuth)提出,详情可参考维基百科。】

本书分为两册。第一分册为逻辑基础,发展了所需的形式化工具。第二分册为编程语言基础, 介绍了操作语义的基本方法。

个人评论

从 2013 年开始,我在爱丁堡大学为四年制本科生和研究生教授编程语言的类型和语义的课程。 该课程的早期版本基于 Benjamin Pierce 的著作 TAPL。我的版本则基于 Pierce 的后续教材《软件基础》(英文版 Software Foundations),此书为 Pierce 与他人合著,基于 Coq 编写。正如 Pierce 在 ICFP 的主题演讲 Lambda, The Ultimate TA 中所言,我也相信基于证明助理的课程会对学习有所帮助。

然而有了五年的教学经验后,我发现 Coq 并不是最好的授课载体。 对于学习编程语言理论基础而言,我们花费了太多课程去专门学习证明推导的策略(Tactic)。 每个概念都需要学习两遍:例如,在学过一遍积数据类型(Product Data Type)之后, 我们还要再学一遍与之对应的合取(Conjunction)的引入(Introduction)和消除(Elimination)策略。 Coq 用来生成归纳假设(Induction Hypothesis)的规则有时看起来很玄学。而 notation 构造则允许直观但灵活多变的语法,同一个概念总是有两个名字有时会令人迷惑, 例如,subst N x MN [x := M]。策略的名字时短时长;标准库中的命名约定则非常不一致。 命题即类型作为证明的基础虽然存在,但却被雪藏了。

我发现自己热衷于用 Agda 重构此课程。在 Agda 中,我们不再需要学习策略了: 这里只有依值类型编程,简单纯粹。我们总是通过构造子来引入,通过模式匹配来消除。 归纳不再是谜之独立的概念,它与我们熟悉的递归概念直接对应。混缀语法十分灵活, 但每个概念只需要一个名字,例如代换就是 _[_:=_]。标准库虽不完美,但它的一致性却很合理。 命题即类型作为证明的基础则被骄傲地展示了出来。

不过,此前还没有用 Agda 语言描述的编程语言理论教材。虽然 Stump 的 Verified Functional Programming in Agda 涵盖了相关的范围, 但比起编程语言理论,却更多关注于依值类型编程。

本书最初的目标只是简单地改编《软件基础》,保持同样的内容,而只是将代码从 Coq 翻译成 Agda。但五年的课堂经验很快让我明白,自己有一些关于如何展示这些材料的想法。 有人说除非你不得不写书,否则绝对不要写书。而我很快发现这就是一本我不得不写的书。

我很幸运地得到了我的学生 Wen Kokke 的热情帮助。她引导着我站在 Agda 新手的视角写书,还提供了一些十分易用的工具来生成清晰易读的页面。

这里的大部分内容都是在 2018 年上半年的休假期间写的。

—— Philip Wadler,里约热内卢,2018 年 1 月 - 6 月

习题说明

标有推荐的习题是爱丁堡大学使用本教材的课程中学生需要做的。

标有延伸的习题提供了额外的挑战。很少有学生能全部做完,但至少应该尝试一下。

标有实践的习题供想要更多实践的学生练习。

你可以从 Github 上下载本书并将 “– 请将代码写在此处” 替换为你的习题解答。 此外,你也可能在课程作业中收到需要编辑的文件,其中包含了练习。

你在解题时或许需要导入库函数,也可能需要将 PLFA 设置为 Agda 库,方法可参考 读者指南

请不要在公共空间中发表习题的答案。

GitHub 上有一个发布了部分习题答案的非公开代码仓库。如果你想获得访问,请联系 Philip Wadler。

使用说明

CIpre-commit.ci statusRelease Versionagdastandard-library

读者指南

《编程语言基础:Agda 语言描述》的使用方法与《Programming Language Foundations in Agda》一致。

本书可访问 PLFA-zh 在线阅读。

要参与翻译,请先阅读翻译规范

你可以在线阅读 PLFA,无需安装任何东西。 然而,如果你想要交互式编写代码或完成习题,那么就需要几样东西:

PLFA 只针对特定的 Agda 和 标准库版本进行了测试,相应版本已在前面的徽章中指明。 Agda 和标准库变化得十分迅速,而这些改变经常搞坏 PLFA,因此使用旧版或新版通常会出现问题。

Agda 和 Agda 标准库有多个版本。如果你使用了包管理器(如 Homebrew 或者 Debian apt),Agda 的版本可能不是最新的。除此之外,Agda 仍然在活跃的开发之中,如果你从 GitHub 上安装了开发版本,开发者的新变更也可能让这里的代码出现问题。 因此,使用上述版本的 Agda 和 Agda 标准库很重要。

macOS 平台:安装 XCode 命令行工具

在 macOS 平台,你需要安装 XCode 命令行工具。 在大多数 macOS 系统版本上,你可以用下面的命令安装它们:

xcode-select --install

安装 Git

你可以使用下面的命令来检查你是否安装了 Git。

git --version

如果你没有 Git,请参阅 Git 下载页面

安装 GHC 和 Cabal

Agda 是用 Haskell 写成的,所以为了安装它我们需要 Glorious Haskell Compiler 和它的包管理器 Cabal。PLFA 应该在任何 >=8.10 的 GHC 版本下运行,在 8.10 到 9.8 版本下完成测试。我们建议使用 ghcup 来安装两者。 在 ghcup 安装好之后,输入下列命令:

ghcup install ghc 9.4.8
ghcup install cabal recommended

ghcup set ghc 9.4.8
ghcup set cabal recommended

或使用 ghcup tui 来『设置』合适的工具。

安装 Agda

安装 Agda 最简单的方法是通过 Cabal。PLFA 使用 Agda 版本 2.6.3。运行下面的命令:

cabal update
cabal install Agda-2.6.3

这一步会消耗很长时间和很多内存来完成。

如果你遇到了问题,或者想参考替代的方法,可参阅 Agda 安装指引

如果你愿意,你可以测试 Agda 是否已正确安装

安装 PLFA 和 Agda 标准库

我们建议您使用下面的命令把 PLFA 安装至你的家目录:

git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/plfa/plfa.github.io plfa

PLFA 包括了所需要的 Agda 标准库版本,如果你在克隆时使用了 --recurse-submodule 选项,你在 standard-library 文件夹中已经有了 Agda 标准库!

最后,我们需要让 Agda 知道如何找到标准库。 你需要两个配置文件,一个用于指定库的路径,一个用于指定默认载入的库。

在 macOS 和 Unix 上,如果 PLFA 已经安装至 Home 目录,且你没有希望保存的配置文件,运行下面的命令:

mkdir -p ~/.agda
cp ~/plfa/data/dotagda/* ~/.agda

这条命令提供了 Agda 标准库,也让 PLFA 可以当作一个 Agda 库来使用。

否则,你需要手动编辑相关的配置文件。两者都位于 AGDA_DIR 目录下。 在 UNIX 和 macOS 平台,AGDA_DIR 默认为 ~/.agda。在 Windows 平台,AGDA_DIR 一般默认为 %AppData%\agda,而 %AppData% 默认为 C:\Users\USERNAME\AppData\Roaming

  • 如果 AGDA_DIR 文件夹不存在,创建它。
  • AGDA_DIR 中,创建一个纯文本文件 libraries,内容为 /path/to/standard-library.agda-lib (即上文中记录的路径)。 这个文件让 Agda 知道有一个名为 standard-library 的库可用。
  • AGDA_DIR 中,创建一个纯文本文件 defaults,内容__仅__为 standard-library 这一行。
  • 如果你想导入书中的模块,那么需要将 PLFA 设置为 Agda 库。假设 PLFA 是 PLFA 的根目录,完成此设置需要将 PLFA/src/plfa.agda-lib 作为单独的一行添加到 AGDA_DIR/libraries,并将 plfa 作为单独的一行添加到 AGDA_DIR/defaults

关于放置标准库的更多信息可以参阅 Agda 文档中的库管理

设置 Agda 的编辑器

Emacs

推荐的 Agda 编辑器是 Emacs。安装 Emacs 可以用下面的方法:

确保你可以用你安装的版本打开、编辑、保存文件。GNU Emacs 网站上的 Emacs 向导描述了如果打开 Emacs 安装中的教程。

Agda 自带了 Emacs 编辑器支持,如果你安装了 Agda,运行下面的命令来配置 Emacs:

agda-mode setup
agda-mode compile

如果你已经是 Emacs 用户,并有自己的设置,你会发现 setup 命令向你的 .emacs 文件中追加了配置,来配合你已有的设置。

在 Emacs 中自动加载 agda-mode

从版本 2.6.0 开始,Agda 支持 Markdown 风格的文学编程,文件使用 .lagda.md 扩展名。 该扩展名的一个问题就是 Emacs 默认会进入 Markdown 编辑模式。 而为了让 agda-mode 在你打开 .agda.lagda.md 文件时自动加载, 请将以下内容放到你的 Emacs 配置文件中:

;; auto-load agda-mode for .agda and .lagda.md
(setq auto-mode-alist
   (append
     '(("\\.agda\\'" . agda2-mode)
       ("\\.lagda.md\\'" . agda2-mode))
     auto-mode-alist))

如果你的配置中已有了改变 auto-mode-alist 的设置,将上述内容放置在已有的设置__之后__,或者将其与已有设置合并(如果你对 Emacs Lisp 足够了解)。 Emacs 的配置文件通常位于 ~/.emacs~/.emacs.d/init.el,然而 Aquamacs 用户需要将启动设置放到位于 ~/Library/Preferences/Aquamacs Emacs/PreferencesPreferences.el 文件中。 对于 Windows 平台,请参阅 GNU Emacs 文档 来寻找配置文件的位置。

可选:在 Emacs 中使用 Mononoki 字体

Agda 中的很多重要符号是用 Unicode 来表示的,因此用来显示和编辑 Agda 的字体需要正确地显示这些符号。最重要的是你使用的字体需要有好的 Unicode 字符支持。我们推荐 Mononoki, 其他的备选字体有 Source Code ProDejaVu Sans MonoFreeMono

你可以直接从此网站 下载并安装 Mononoki。对于大多数系统来说,安装字体只是简单的下载 .otf 或者 .ttf 文件。 如果你的包管理器提供了 Mononoki 的包,那样可能更加简单。 例如,macOS 的 Homebrew 提供了 font-mononiki 包;Debian 的 APT 提供了 fonts-mononoki 包。 将下面的内容加入 Emacs 配置文件,可以把 Mononoki 设置为 Emacs 的默认字体:

;; default to mononoki
(set-face-attribute 'default nil
                    :family "mononoki"
                    :height 120
                    :weight 'normal
                    :width  'normal)
检查 agda-mode 是否正确安装

在 Emacs 中打开本书的第一章 (plfa/src/plfa/part1/Naturals.lagda.md),输入 C-c C-l 来载入和类型检查这个文件。

在 Emacs 中使用 agda-mode

要加载文件并对其执行类型检查,请使用 C-c C-l

Agda 的编辑是通过使用「」来交互的,它表示程序中尚未填充的片段。 如果用问号作为表达式,并用 C-c C-l 加载缓冲区,Agda 会将问号替换为一个「洞」。 当光标在洞中时,你可以做以下这些事情:

  • C-c C-c: 分项(询问变量名,case)
  • C-c C-空格:填洞
  • C-c C-r:用构造子精化(refine)
  • C-c C-a:自动填洞(automatic)
  • C-c C-,:目标类型和语境
  • C-c C-.:目标类型,语境,以及推断的类型

更多细节请见 emacs-mode 文档

如果你想在 Agda 代码的侧边而非底栏查看信息,那么可以这样做:

  • 打开 Agda 文件并按 C-c C-l
  • C-x 1 来仅显示当前 Agda 文件;
  • C-x 3 来垂直分割窗口;
  • 将光标移动到右侧窗口;
  • C-x b 并输入「Agda information」切换到该缓冲区。

现在,Agda 的错误消息将会在代码右侧显示,而非被挤压在底部的狭小空间里。

在 Emacs 中使用 agda-mode 输入 Unicode 字符

当你书写 Agda 代码时,你需要输入标准键盘上没有的字符。Emacs 的「Agda-mode」定义了字符翻译来让这件事更容易:当你输入特定普通字符的序列时,Emacs 会在 Agda 文件中使用对应的特殊字符来替换。

例如,我们可以在之前的 .agda 测试文件中加入一条注释。 比如说,我们想要加入下面的注释:

{- I am excited to type ∀ and → and ≤ and ≡ !! -}

前几个字符都是普通字符,我们可以如往常的方式输入它们……

{- I am excited to type

在最后一个空格之后,我们无法在键盘上找到 ∀ 这个键。这个字符的输入序列是四个字符 \all,所以我们输入这四个字符,当我们完成时,Emacs 会把它们替换成我们想要的……

{- I am excited to type ∀

我们继续输入其他字符。有时字符会在输入时发生变化,因为一个字符的输入序列是另一个字符输入序列的前缀。 在我们输入箭头时会出现这样的情况,它的输入序列是 \->。在输入 \- 之后我们会看到……

{- I am excited to type ∀ and -

……因为输入序列 \- 对应了一定长度的短横。当我们输入 > 时,­ 变成了 的输入序列是 \<= 的是 \==

{- I am excited to type ∀ and → and ≤ and ≡

最后几个字符又回归了普通字符……

{- I am excited to type ∀ and → and ≤ and ≡ !! -}

如果你在 Emacs 中输入 Unicode 时遇到了问题,可以在每章的最后找到当前章节中用到的 Unicode 字符。

带有 agda-mode 的 Emacs 提供了很多有用的命令,其中有两个对处理 Unicode 字符时特别有用。 支持的字符的完整列表可使用 agda-input-show-translations 命令查看:

M-x agda-input-show-translations

agda-mode 支持的所有字符都会列出来。

如果你想知道如何在 agda 文件输入一个特定的 Unicode 字符,请将光标移动到该字符上并输入以下命令:

M-x quail-show-key

你会在迷你缓冲区中看到该字符对应的按键序列。

Spacemacs

Spacemacs 是一个「社区引领的 Emacs 版本」,对 Emacs 和 Vim 的编辑方式都有很好的支持。它自带集成了 agda-mode,所需的只是将 .spacemacs 中启用 Agda 支持。

Visual Studio Code

Visual Studio Code 是一个微软开发的开源代码编辑器。 Visual Studio 市场中有 Agda 插件

Getting Started for Contributors

If you plan to build PLFA locally, please refer to Contributing for additional instructions.

第一分册:逻辑基础

Naturals: 自然数

module plfa.part1.Naturals where

夜空中的星星不计其数,但只有不到五千颗是肉眼可见的。 可观测宇宙中则包含大约 7*10^22 颗恒星。

星星虽多,但却是有限的,而自然数是无限的。就算用自然数把所有的星星都数尽了, 剩下的自然数也和开始的一样多。

自然数是一种归纳数据类型(Inductive Datatype)

大家都熟悉自然数,例如:

0
1
2
3
...

等等。我们将自然数的类型(Type)记作 ,并称 0123 等数 是类型 值(Value),表示为 0 : ℕ1 : ℕ2 : ℕ3 : ℕ 等等。

自然数集是无限的,然而其定义只需寥寥几行即可写出。下面是用两条推导规则(Inference Rules)定义的自然数:

--------
zero : ℕ

m : ℕ
---------
suc m : ℕ

以及用 Agda 定义的自然数:

data  : Set where
  zero : 
  suc  :   

其中 是我们定义的数据类型(Datatype)的名字,而 zero(零)和 suc后继,即 Successor 的简写)是该数据类型的构造子(Constructor)

这两种定义说的是同一件事:

  • 起始步骤(Base Case)zero 是一个自然数。
  • 归纳步骤(Inductive Case):如果 m 是一个自然数,那么 suc m 也是。

此外,这两条规则给出了产生自然数唯一的方法。因此,可能的自然数包括:

zero
suc zero
suc (suc zero)
suc (suc (suc zero))
...

我们将 zero 简写为 0;将 suc zero,零的后继数, 也就是排在零之后的自然数,简写为 1;将 suc (suc zero),也就是 suc 1, 即一的后继数,简写为 2;将二的后继数简写为 3;以此类推。

练习 seven(实践)

请写出 7 的完整定义。

-- 请将代码写在此处。

你需要为 seven 给出类型签名以及定义。在 Emacs 中使用 C-c C-l 来让 Agda 重新加载。

推导规则分析

我们来分析一下刚才的两条推导规则。每条推导规则包含写在一条水平直线上的 零条或多条判断(Judgment),称之为假设(Hypothesis);以及写在 直线下的一条判断,称之为结论(Conclusion)。第一条规则是起始步骤:它没 有任何假设,其结论断言 zero 是一个自然数。第二条规则是归纳步骤:它有 一条假设,即 m 是自然数,而结论断言 suc m 也是一个自然数。

Agda 定义分析

现在分析一下 Agda 的定义。关键字 data 表示这是一个归纳定义, 也就是用构造子定义一个新的数据类型。

ℕ : Set

表示 是新的数据类型的名字,它是一个 Set,也就是在 Agda 中对类型的称呼。 关键字 where 用于分隔数据类型的声明和构造子的声明。 每个构造子的声明独占一行,用缩进来指明它所属的 data 声明。

zero : ℕ
suc  : ℕ → ℕ

这两行给出了构造子 zerosuc 的类型签名(Signature)。 它们表示 zero 是一个自然数,suc 则取一个自然数作为参数,返回另一个自然数。

读者可能注意到 在键盘上没有对应的按键。它们是 Unicode(统一码)中的符号。每一章的结尾都会有本章节引入的 Unicode 符号列表,以及在 Emacs 编辑器中输入它们的方法。

创世故事

我们再看一下自然数的定义规则:

  • 起始步骤(Base Case)zero 是一个自然数。
  • 归纳步骤(Inductive Case):如果 m 是一个自然数,那么 suc m 也是。

等等!第二行用自然数定义了自然数,这怎么能行?这个定义难道 不会像「脱欧即是脱欧」一样无用吗?

【译注:「脱欧即是脱欧」是英国首相特蕾莎·梅提出的一句口号。】

实际上,不必通过自我指涉,我们的自然数定义也是可以被赋予意义的。 我们甚至只需要处理有限的集合,而不必涉及无限的自然数集。

我们可以将这个过程比作一个创世故事。起初,我们对自然数一无所知:

-- 起初,世上没有自然数。

现在,我们对所有已知的自然数应用之前的规则。起始步骤告诉我们 zero 是 一个自然数,所以我们将它加入已知自然数的集合中。归纳步骤告诉我们如果 「昨天的」m 是一个自然数,那么「今天的」suc m 也是一个自然数。我们在 今天之前并不知道任何自然数,所以归纳步骤在此处不适用。

-- 第一天,世上有了一个自然数。
zero : ℕ

然后我们重复此过程。今天我们知道昨天的所有自然数,以及任何 通过规则添加的数。起始步骤依然告诉我们 zero 是一个自然数,我们 已经知道这件事了。而如今归纳步骤告诉我们,由于 zero 在昨天是自然数, 那么 suc zero 在今天也是自然数:

-- 第二天,世上有了两个自然数。
zero : ℕ
suc zero : ℕ

我们再次重复此过程。现在归纳步骤告诉我们,由于 zerosuc zero 都是自然 数,因此 suc zerosuc (suc zero) 也是自然数。我们已经知道 suc zero 是自然数了,而后者 suc (suc zero) 是新加入的。

-- 第三天,世上有了三个自然数。
zero : ℕ
suc zero : ℕ
suc (suc zero) : ℕ

此时规律已经很明显了。

-- 第四天,世上有了四个自然数。
zero : ℕ
suc zero : ℕ
suc (suc zero) : ℕ
suc (suc (suc zero)) : ℕ

此过程可以继续下去。在第 n 天会有 n 个不同的自然数。每个自然数都会在 某一天出现。具体来说,自然数 n 在第 n+1 天首次出现。至此,我们并没有使 用自然数集来定义其自身,而是根据第 n 天的数集定义了第 n+1 天的数集。

像这样的过程被称作是归纳的(Inductive)。我们从一无所有开始,通过应用将 一个有限集合转换到另一个有限集合的规则,逐步生成潜在无限的集合。

定义了零的规则之所以被称作起始步骤,是因为它在我们还不知道其它自然数时 就引入了一个自然数。定义了后继数的规则之所以被称作归纳步骤,则是因为它在 已知自然数的基础上引入了更多自然数。其中,起始步骤的重要性不可小觑。如果 只有归纳步骤,那么第一天就没有任何自然数。第二天,第三天,无论多久也依旧没有。 一个没有起始步骤的归纳定义是无用的,就像「脱欧即是脱欧」一样。

哲学和历史

哲学家发现,我们对「第一天」「第二天」等说法的使用暗含了对自然数的理解。 在这个层面上,我们对自然数的定义也许某种程度上可以说是循环的,但我们不必为此烦恼。 每个人对自然数都有良好的非形式化的理解,而我们可以将它作为形式化描述自然数的基础。

尽管从人类开始计数起,自然数就被人所认知,然而其归纳定义却是近代的事情。 这可以追溯到理查德·戴德金(Richard Dedekind)于 1888 年发表的论文 Was sind und was sollen die Zahlen?”(《数是什么,应该是什么?》), 以及朱塞佩·皮亚诺(Giuseppe Peano)于次年发表的著作 “Arithmetices principia, nova methodo exposita”(《算术原理:用一种新方法呈现》)。

编译指令

在 Agda 中,任何跟在 -- 之后或者由 {--} 包裹的文字都会被视为 注释(Comment)。一般的注释对代码没有任何作用,但有一种例外, 这种注释被称作编译指令(Pragma),由 {-##-} 包裹。

{-# BUILTIN NATURAL  #-}

这一行告诉 Agda 数据类型 对应了自然数,然后编写者就可以将 zero 简写 为 0,将 suc zero 简写为 1,将 suc (suc zero) 简写为 2 了,以此类推。 必须要向编译指令给出之前声明的类型(本例中为 ),该类型有且只有两个构造子, 其中一个没有参数(本例中为 zero),另一个只接受一个所给定类型的参数(本例中为 suc)。

在启用上述简写的同时,这条编译指令也会用 Haskell 的任意精度整数类型 来提供更高效的自然数内部表示。用 zerosuc 表示自然数 n 要占用正比 于 n 的空间,而将其表示为 Haskell 中的任意精度整数只会占用正比于 n 的对数的空间。

导入模块

我们很快就能写一些包含自然数的等式了。在此之前,我们需要从 Agda 标准库 中导入相等性(Equality)的定义和用于等式推理的记法:

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl)
open Eq.≡-Reasoning using (begin_; _≡⟨⟩_; _∎)

第一行代码将标准库中定义了相等性的模块导入到当前作用域(Scope)中, 并将其命名为 Eq。第二行打开了这个模块,也就是将所有在 using 从句中指定的名称添加到当前作用域中。此处添加的名称有相等性运算符 _≡_ 和两个项相等的证据 refl。第三行选取的模块提供了用于等价关系推理的运算符,并将 using 从句中指定的名称添加到当前作用域。此处添加的名称有 begin__≡⟨⟩__∎。我们后面会看到这些运算符的使用方法。现在暂且把它们当作现成的 工具来使用,不深究其细节。但我们会在相等性一章中 学习它们的具体定义。

Agda 用下划线来标注项(Term)在中缀(Infix)或混缀(Mixfix)运算符中项出现的位置。 因此,_≡__≡⟨⟩_ 是中缀的(运算符写在两个项之间),而 begin_ 是前缀的 (运算符写在项之前),_∎ 则是后缀的(运算符写在项之后)。

括号和分号是少有的几个不能在名称中出现的的字符,于是我们在 using 列表中不需要额外的空格来消除歧义。

自然数的运算是递归函数

既然我们有了自然数,那么可以用它们做什么呢?比如,我们能定义 加法和乘法之类的算术运算吗?

我儿时曾花费了大量的时间来记忆加法表和乘法表。最开始,运算规则看起来很 复杂,我也经常犯错。在发现递归(Recursion)时,我如同醍醐灌顶。 有了这种简单的技巧,无数的加法和乘法运算只用几行就能概括。

这是用 Agda 编写的加法定义:

_+_ :     
zero + n = n
(suc m) + n = suc (m + n)

我们来分析一下它的定义。加法是一种中缀运算符,其名为 _+_,其中参数的 位置用下划线表示。第一行指定了运算符的类型签名。类型 ℕ → ℕ → ℕ 表示 加法接受两个自然数作为参数,并返回一个自然数。中缀记法只是函数应用的简写, m + n_+_ m n 这两个项是等价的。

它的定义包含一个起始步骤和一个归纳步骤,与自然数的定义对应。起始步骤说明 零加上一个数仍返回这个数,即 zero + n 等于 n。归纳步骤说明一个数的后继数 加上另一个数返回两数之和的后继数,即 (suc m) + n 等于 suc (m + n)。在加法的 定义中,构造子出现在了等式左边,我们称之为模式匹配(Pattern Matching)

如果我们将 zero 写作 0,将 suc m 写作 1 + m,上面的定义就变成了 两个熟悉的等式。

 0       + n  ≡  n
 (1 + m) + n  ≡  1 + (m + n)

因为零是加法的幺元,所以第一个等式成立。又因为加法满足结合律,所以 第二个等式也成立。加法结合律的一般形式如下,说明运算结果与括号位置无关。

 (m + n) + p  ≡  m + (n + p)

将上面第三个等式中的 m 换成 1n 换成 mp 换成 n,我们就 得到了第二个等式。我们用等号 = 表示定义,用 断言两个已定义的事物相等。

加法的定义是递归(Recursive)的,因为在最后一行我们用加法定义了加法。 与自然数的归纳定义类似,这种表面上的循环性并不会造成问题,因为较大 的数相加是用较小的数相加定义的。这样的定义被称作是良基的(Well founded)

例如,我们来计算二加三:

_ : 2 + 3  5
_ =
  begin
    2 + 3
  ≡⟨⟩    -- 展开为
    (suc (suc zero)) + (suc (suc (suc zero)))
  ≡⟨⟩    -- 归纳步骤
    suc ((suc zero) + (suc (suc (suc zero))))
  ≡⟨⟩    -- 归纳步骤
    suc (suc (zero + (suc (suc (suc zero)))))
  ≡⟨⟩    -- 起始步骤
    suc (suc (suc (suc (suc zero))))
  ≡⟨⟩    -- 简写为
    5
  

我们可以按需展开简写,把同样的推导过程写得更加紧凑。

_ : 2 + 3  5
_ =
  begin
    2 + 3
  ≡⟨⟩
    suc (1 + 3)
  ≡⟨⟩
    suc (suc (0 + 3))
  ≡⟨⟩
    suc (suc 3)
  ≡⟨⟩
    5
  

第一行取 m = 1n = 3 匹配了归纳步骤,第二行取 m = 0n = 3 匹配了归纳步骤,第三行取 n = 3 匹配了起始步骤。

以上两个推导过程都由一个类型签名(包含冒号 : 的一行)和一个提供对应类型 的项的绑定(Binding)(包含等号 = 的一行及之后的部分)组成。在编写代码时 我们用了虚设名称 _。虚设名称可以被重复使用,在举例时非常方便。除了 _ 之外 的名称在一个模块里只能被定义一次。

这里的类型是 2 + 3 ≡ 5,而该等式写成列表形式的等式链的项,提供了类型中表示 等式成立的证据(Evidence)。该等式链由 begin 开始,以 结束( 可读作「qed(证毕)」或「tombstone(墓碑符号)」,后者来自于其外观), 并由一系列 ≡⟨⟩ 分隔的项组成。

注意,上面的证明以彩色显示,表示 Agda 已经处理并接受了这些代码, 因此可以保证它们没有类型错误。在 Agda 中处理后,Emacs 源文件中会以同样的颜色显示。 在 Emacs 中,右键单击任何彩色的符号,例如 +suc,就会跳转到该符号的定义。

其实,以上两种证明都比实际所需的要长,下面的证明就足以让 Agda 满意了。

_ : 2 + 3  5
_ = refl

Agda 知道如何计算 2 + 3 的值,也可以立刻确定这个值和 5 是一样的。如果一个 二元关系(Binary Relation)中每个值都和自己相关,我们称这个二元关系满足 自反性(Reflexivity)。在 Agda 中,一个值等于其自身的证据写作 refl

在等式链中,Agda 只检查每个项是否都能化简为相同的值。如果我们打乱等式顺序, 省略或者加入一些额外的步骤,证明仍然会被接受。我们需要自己来保证等式的顺序 便于理解。

在这里,2 + 3 ≡ 5 是一个类型,等式链(以及 refl)都是这个类型的项。 换言之,我们也可以把每个项都看作断言 2 + 3 ≡ 5证据。这种解释的 对偶性——类型即命题,项即证据——是我们在 Agda 中形式化各种概念的核心, 也是贯穿本书的主题。

注意,当我们使用证据这个词时不容一点含糊。这里的证据确凿不移, 不像法庭上的证词一样必须被反复权衡以决定证人是否可信。 我们也会使用证明一词表达相同的意思,在本书中这两个词可以互换使用。

练习 +-example(实践)

计算 3 + 4,将你的推导过程写成等式链,为 + 使用等式。

-- 请将代码写在此处。

乘法

一旦我们定义了加法,我们就可以将乘法定义为重复的加法。

_*_ :     
zero    * n  =  zero
(suc m) * n  =  n + (m * n)

计算 m * n 返回的结果是 mn 之和。

重写定义再一次给出了两个熟悉的等式:

0       * n  ≡  0
(1 + m) * n  ≡  n + (m * n)

因为零乘任何数都是零,所以第一个等式成立。因为乘法对加法有分配律,所以 第二个等式也成立。乘法对加法的分配律的一般形式如下:

(m + n) * p  ≡  (m * p) + (n * p)

将上面第三个等式中的 m 换成 1n 换成 mp 换成 n,再根据 一是乘法的幺元,也就是 1 * n ≡ n,我们就得到了第二个等式。

这个定义也是良基的,因为较大的数相乘是用较小的数相乘定义的。

例如,我们来计算二乘三:

_ =
  begin
    2 * 3
  ≡⟨⟩    -- 归纳步骤
    3 + (1 * 3)
  ≡⟨⟩    -- 归纳步骤
    3 + (3 + (0 * 3))
  ≡⟨⟩    -- 起始步骤
    3 + (3 + 0)
  ≡⟨⟩    -- 化简
    6
  

第一行取 m = 1n = 3 匹配了归纳步骤,第二行取 m = 0n = 3 匹配了归纳步骤,最后第三行取 n = 3 匹配了起始步骤。在这里我们省略了 _ : 2 * 3 ≡ 6 的签名,因为它很容易从对应的项推导出来。

练习 *-example(实践)

计算 3 * 4,将你的推导过程写成等式链,为 * 使用等式。 (不必写出 + 求值的每一步。)

-- 请将代码写在此处。
练习 _^_(推荐)

根据以下等式写出乘方的定义。

m ^ 0        =  1
m ^ (1 + n)  =  m * (m ^ n)

检查 3 ^ 4 是否等于 81

-- 请将代码写在此处。

饱和减法

我们也可以定义减法。由于没有负的自然数,如果被减数比减数小, 我们就将结果取零。这种针对自然数的减法变种称作饱和减法(Monus,由 minus 修改而来)

饱和减法是我们首次在定义中对两个参数都使用模式匹配:

_∸_ :     
m      zero   =  m
zero   suc n  =  zero
suc m  suc n  =  m  n

我们可以通过简单的分析来说明所有的情况都被考虑了。

  • 考虑第二个参数。
    • 如果它是 zero,应用第一个等式。
    • 如果它是 suc n,考虑第一个参数。
      • 如果它是 zero,应用第二个等式。
      • 如果它是 suc m,应用第三个等式。

Agda will raise an error if all the cases are not covered. As with addition and multiplication, the recursive definition is well founded because monus on bigger numbers is defined in terms of monus on smaller numbers.

例如,我们来计算三减二:

_ =
  begin
    3  2
  ≡⟨⟩
    2  1
  ≡⟨⟩
    1  0
  ≡⟨⟩
    1
  

我们没有使用第二个等式,但是如果被减数比减数小,我们还是会用到它。

_ =
  begin
    2  3
  ≡⟨⟩
    1  2
  ≡⟨⟩
    0  1
  ≡⟨⟩
    0
  

我们对饱和减法的定义确保了只有一条等式可以应用。 假设我们将第二条以下文取而代之

zero  ∸ n  =  zero

那样就不清楚 Agda 应该使用第一条或者第二条来简化 zero ∸ zero。 在这样的情况下,两者都可以相同的答案 zero,但这不一定是普遍的情况。 将

{-# OPTIONS --exact-split #-}

写在文件的开始可以让 Agda 在不同情况相互重叠时产生一个错误, 有些时候这会有帮助。我们会在逻辑连接符部分 展示一个这样的例子。

练习 ∸-example₁∸-example₂(推荐)

计算 5 ∸ 33 ∸ 5,将你的推导过程写成等式链。

-- 请将代码写在此处。

优先级

我们经常使用优先级(Precedence)来避免书写大量的括号。 函数应用比其它任何运算符都结合得更紧密(即有更高的优先级),所以我们 可以用 suc m + n 来表示 (suc m) + n。另一个例子是,我们说乘法比 加法结合得更紧密,所以可以用 n + m * n 来表示 n + (m * n)。我们有 时候也说加法是左结合的,所以可以用 m + n + p 来表示 (m + n) + p

在 Agda 中,中缀运算符的优先级和结合性需要被声明:

infixl 6  _+_  _∸_
infixl 7  _*_

它声明了运算符 _+__∸_ 的优先级为 6,运算符 _*_ 的优先级 为 7。因为加法和饱和减法的优先级更低,所以它们结合得不如乘法紧密。 infixl 意味着三个运算符都是左结合的。编写者也可以用 infixr 来表示 某个运算符是右结合的,或者用 infix 来表示总是需要括号来消除歧义。

柯里化

我们曾将接受两个参数的函数表示成「接受第一个参数,返回接受第二个 参数的函数」的函数。这种技巧叫做柯里化(Currying)

与 Haskell 和 ML 等函数式语言类似,Agda 在设计时就考虑了让柯里化更加易用。 函数箭头是右结合的,而函数应用是左结合的。

比如

ℕ → ℕ → ℕ 表示 ℕ → (ℕ → ℕ)

_+_ 2 3 表示 (_+_ 2) 3

_+_ 2 这个项表示一个「将参数加二」的函数,因此将它应用到三就得到了五。

柯里化是以哈斯凯尔·柯里(Haskell Curry)的名字命名的,编程语言 Haskell 也是。 柯里的工作可以追溯到 19 世纪 30 年代。当我第一次了解到柯里化时, 有人告诉我柯里化的命名是个归因错误,因为在 20 年代同样的想法就已经被 Moses Schönfinkel 提出了。我也听说过这样一个笑话:「(柯里化)本来该命名成 Schönfinkel 化的,但是咖喱(Curry)更好吃」。直到之后我才了解到, 这个归因错误的解释本身也是个归因错误。柯里化的概念早在戈特洛布·弗雷格 (Gottlob Frege)发表于 1879 年的 “Begriffsschrift”(《概念文字》)中就出现了。

又一个创世故事

和归纳定义中用自然数定义了自然数一样,递归定义也用加法定义了加法。

同理,无需利用循环性,我们的加法定义也是可以被赋予意义的。 为此,我们需要将加法的定义规约到用于判断相等性的等价的推导规则上来。

n : ℕ
--------------
zero + n  =  n

m + n  =  p
---------------------
(suc m) + n  =  suc p

假设我们已经定义了自然数的无限集合,指定了判断 n : ℕ 的意义。 第一条推导规则是起始步骤。它断言如果 n 是一个自然数,那么零加上它得 n。 第二条推导规则是归纳步骤。它断言如果 m 加上 np,那么 suc m 加 上 nsuc p

我们同样借创世故事来帮助理解,不过这次关注的是关于加法的判断。

-- 起初,我们对加法一无所知。

现在对所有已知的判断应用之前的规则。起始步骤告诉我们,对于 每个自然数 n 都有 zero + n = n,因此我们添加所有的这类等式。 归纳步骤告诉我们,如果「昨天」有 m + n = p,那么「今天」 就有 suc m + n = suc p。在今天之前,我们不知道任何关于加法的等式, 因此这条规则不会给我们任何新的等式。

-- 第一天,我们知道了 0 为被加数的加法。
0 + 0 = 0     0 + 1 = 1    0 + 2 = 2     ...

然后我们重复这个过程。今天我们知道来自昨天的所有等式,以及任何通过 规则添加的等式。起始步骤没有告诉我们任何新东西,但是归纳步骤添加了更多的等式。

-- 第二天,我们知道了 0,1 为被加数的加法。
0 + 0 = 0     0 + 1 = 1     0 + 2 = 2     0 + 3 = 3     ...
1 + 0 = 1     1 + 1 = 2     1 + 2 = 3     1 + 3 = 4     ...

我们再次重复这个过程:

-- 第三天,我们知道了 0,1,2 为被加数的加法。
0 + 0 = 0     0 + 1 = 1     0 + 2 = 2     0 + 3 = 3     ...
1 + 0 = 1     1 + 1 = 2     1 + 2 = 3     1 + 3 = 4     ...
2 + 0 = 2     2 + 1 = 3     2 + 2 = 4     2 + 3 = 5     ...

此时规律已经很明显了:

-- 第四天,我们知道了 0,1,2,3 为被加数的加法。
0 + 0 = 0     0 + 1 = 1     0 + 2 = 2     0 + 3 = 3     ...
1 + 0 = 1     1 + 1 = 2     1 + 2 = 3     1 + 3 = 4     ...
2 + 0 = 2     2 + 1 = 3     2 + 2 = 4     2 + 3 = 5     ...
3 + 0 = 3     3 + 1 = 4     3 + 2 = 5     3 + 3 = 6     ...

此过程可以继续下去。在第 m 天我们将知道所有被加数小于 m 的等式。

如上所示,归纳定义和递归定义的的推导过程十分相似。它们就像一枚硬币的两面。

有限的创世故事

前面的创世故事是用分层的方式讲述的。首先,我们创造了自然数的无限集合。 然后,我们构造加法的实例时把自然数集视为现成的,所以即使在第一天我 们也有一个无限的实例集合。

然而,我们也可以选择同时构造自然数集和加法的实例。这样在任何一天都只会有 一个有限的实例集合。

-- 起初,我们一无所知。

现在,对我们已知的所有判断应用之前的规则。只有自然数的起始步骤适用:

-- 第一天,我们知道了零。
0 : ℕ

我们再次应用所有的规则。这次我们有了一个新自然数,和加法的第一个等式。

-- 第二天,我们知道了一和所有和为零的加法算式。
0 : ℕ
1 : ℕ    0 + 0 = 0

然后我们重复这个过程。我们通过加法的起始步骤得到了一个等式,也通过在前一天 的等式上应用加法的归纳步骤得到了一个等式:

-- 第三天,我们知道了二和所有和为一的加法算式。
0 : ℕ
1 : ℕ    0 + 0 = 0
2 : ℕ    0 + 1 = 1   1 + 0 = 1

此时规律已经很明显了:

-- 第四天,我们知道了三和所有和为二的加法算式。
0 : ℕ
1 : ℕ    0 + 0 = 0
2 : ℕ    0 + 1 = 1   1 + 0 = 1
3 : ℕ    0 + 2 = 2   1 + 1 = 2    2 + 0 = 2

在第 n 天会有 n 个不同的自然数和 n × (n-1) / 2 个加法等式。 数字 n 和所有和小于 n 的加法等式在第 n+1 天首次出现。这提供了 一种无限的数据集合及与之相关的等式的有限主义视角。

交互式地编写定义

Agda 被设计为使用 Emacs 作为文本编辑器,二者一同提供了很多能帮助 用户交互式地创建定义和证明的功能。

我们从输入以下代码开始:

_+_ : ℕ → ℕ → ℕ
m + n = ?

问号表示你希望 Agda 帮助你填入这部分代码。如果按下组合键 C-c C-l (按住 Control 键的同时先按 c 键再按 l 键,l 键代表载入 load),这个问号会被替换:

_+_ : ℕ → ℕ → ℕ
m + n = { }0

这对花括号被称作一个洞(Hole),0 是这个洞的编号。洞将会被高亮显示为 绿色(或蓝色)。同时,Emacs 会创建一个窗口显示如下文字:

?0 : ℕ

这表示 0 号洞需要填入一个类型为 的项。按组合键 C-c C-ff 键代表向前 forward)会移动到下一个洞。

我们希望在第一个参数上递归来定义加法。 将光标移至 0 号洞并按 C-c C-cc 键代表分情况讨论 case),你将看见如下提示:

pattern variables to case (empty for split on result):

即「用于分项的模式变量(留空以对结果分项):」。

键入 m 会对名为 m 的变量分项(即自动模式匹配),并将代码更新为:

_+_ : ℕ → ℕ → ℕ
zero + n = { }0
suc m + n = { }1

现在有两个洞了。底部的窗口会告诉你每个洞所需的类型:

?0 : ℕ
?1 : ℕ

移动至 0 号洞,按下 C-c C-, 会显示当前洞所需类型的具体信息,以及 可用的自由变量:

Goal: ℕ
————————————————————————————————————————————————————————————
n : ℕ

这些信息强烈建议了用 n 填入该洞。填入内容后,你可以按下 C-c C-空格 来移除这个洞。

_+_ : ℕ → ℕ → ℕ
zero + n = n
suc m + n = { }1

同理,移动到 1 号洞并按下 C-c C-, 会显示当前洞所需类型的具体信息, 以及可用的自由变量:

Goal: ℕ
————————————————————————————————————————————————————————————
n : ℕ
m : ℕ

移动到一个洞并按下 C-c C-rr 键表示细分 refine)会将一个构造子填入这个洞(如果有唯一的选择的话), 或者告诉你有哪些可用的构造子以供选择。在当前情况下,编辑器会显示如下内容:

Don't know which constructor to introduce of zero or suc

即「不知道在 zerosuc 中该引入哪一个构造子」。

我们将 suc ? 填入并按下 C-c C-空格,它会将代码更新为:

_+_ : ℕ → ℕ → ℕ
zero + n = n
suc m + n = suc { }1

移动到新的洞并按下 C-c C-, 给出了和之前类似的信息:

Goal: ℕ
————————————————————————————————————————————————————————————
n : ℕ
m : ℕ

我们可以用 m + n 填入该洞并按 C-c C-空格 来完成程序:

_+_ : ℕ → ℕ → ℕ
zero + n = n
suc m + n = suc (m + n)

在如此简单的程序上频繁使用交互操作可能帮助不大, 但是同样的技巧能够帮助你构建更复杂的程序。甚至对于加法定义这么简单的程序,使用 C-c C-c 来分项仍然是有用的。

更多编译指令

{-# BUILTIN NATPLUS _+_ #-}
{-# BUILTIN NATTIMES _*_ #-}
{-# BUILTIN NATMINUS _∸_ #-}

以上几行告诉 Agda 这几个运算符和数学中常用的运算符相对应, 以便让它在计算时使用相应的,可处理任意精度整数类型的 Haskell 运算符。 计算 mn 时,用 zerosuc 表示的自然数需要正比于 m 的时间, 而用 Haskell 整数表示的情况下只需要正比于 mn 中较大者的对数的时间。 类似地,计算 mn 时,用 zerosuc 表示的自然数需要正比于 mn 的 时间,而用 Haskell 整数表示的情况下只需要正比于 mn 的对数之和的时间。

练习 Bin(拓展)

使用二进制系统能提供比一进制系统更高效的自然数表示。我们可以用一个比特串来表示一个数:

data Bin : Set where
  ⟨⟩ : Bin
  _O : Bin  Bin
  _I : Bin  Bin

例如,以下比特串

1011

代表数字十一被编码为了

⟨⟩ I O I I

由于前导零的存在,表示并不是唯一的。因此,十一同样可以 表示成 001011,编码为:

⟨⟩ O O I O I I

定义这样一个函数

inc : Bin → Bin

将一个比特串转换成下一个数的比特串。比如,1100 编码了十二,我们就应该有:

inc (⟨⟩ I O I I) ≡ ⟨⟩ I I O O

实现这个函数,并验证它对于表示零到四的比特串都能给出正确结果。

使用以上的定义,再定义一对函数用于在两种表示间转换。

to   : ℕ → Bin
from : Bin → ℕ

对于前者,用没有前导零的比特串来表示正数,并用 ⟨⟩ O 表示零。 验证这两个函数都能对零到四给出正确结果。

-- 请将代码写在此处。

标准库

在每一章的结尾,我们将展示如何在标准库中找到相关的定义。 自然数,它们的构造子,以及用于自然数的基本运算符,都在标准库模块 Data.Nat 中定义:

-- import Data.Nat using (ℕ; zero; suc; _+_; _*_; _^_; _∸_)

正常情况下,我们会以运行代码的形式展示一个导入语句, 这样如果我们尝试导入一个不可用的定义,Agda 就会报错。 不过现在,我们只在注释里展示了这个导入语句。这一章和标准库 都调用了 NATURAL 编译指令。我们是在 上使用,而标准库是在 等价的类型 Data.Nat.ℕ 上使用。这样的编译指令只能被调用一次,因为 重复调用会导致 2 到底是类型 的值还是类型 Data.Nat.ℕ 的 值这样的困惑。重复调用其它的编译指令也会导致同样的问题。基于这个原因, 我们在后续章节中通常会避免使用编译指令。更多关于编译指令的信息可在 Agda 文档中找到。

Unicode

这一章使用了如下的 Unicode 符号:

ℕ  U+2115  双线体大写 N (\bN)
→  U+2192  右箭头 (\to, \r, \->)
∸  U+2238  点减 (\.-)
≡  U+2261  等价于 (\==)
⟨  U+27E8  数学左尖括号 (\<)
⟩  U+27E9  数学右尖括号 (\>)
∎  U+220E  证毕 (\qed)

以上的每一行均包含 Unicode 符号(如 ),对应的 Unicode 码点(如 U+2115), 符号的名称(如 双线体大写 N),以及用于在 Emacs 中键入该符号的按键序列(如 \bN)。

通过 \r 命令可以查看多种右箭头符号。在输入 \r 后,你可以 按左、右、上、下键来查看或选择可用的箭头符号。这个命令会记住你上一次选择的位置, 并在下一次使用时从该字符开始。用于输入左箭头的 \l 命令的用法与此类似。

除了在输入箭头的命令中使用左、右、上、下键以外,以下按键也可以起到相同的作用:

C-b  左(后退一个字符)
C-f  右(前进一个字符)
C-p  上(到上一行)
C-n  下(到下一行)

C-b 表示按 Control + b,其余同理。你也可以直接输入显示的列表中的数字编号来选择。

要查看所支持字符的完整列表,请执行 agda-input-show-translations 命令:

M-x agda-input-show-translations

这样会显示出 agda-mode 中所有支持的字符。我们用 M-x 表示按下 ESC 后再按下 x

如果你想知道如何在 agda 文件中输入一个特定的 Unicode 字符,请将光标移至该字符上, 然后执行 quail-show-key 命令:

M-x quail-show-key

你会在迷你缓冲区中看到输入该字符所需的按键序列。 例如,如果你在 上执行 M-x quail-show-key,就会看到该字符的按键序列为 \.-

Induction: 归纳证明

module plfa.part1.Induction where

归纳会让你对无中生有感到内疚 ……但它却是文明中最伟大的思想之一。 —— Herbert Wilf

现在我们定义了自然数及其运算,下一步是学习如何证明它们满足的性质。 顾名思义,归纳数据类型(Inductive Datatype)是通过归纳(Induction) 来证明的。

导入

我们需要上一章中的相等性,加上自然数及其运算。我们还导入了一些新的运算: congsym_≡⟨_⟩_,之后会解释它们:

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl; cong; sym)
open Eq.≡-Reasoning using (begin_; _≡⟨⟩_; step-≡; _∎)
open import Data.Nat using (; zero; suc; _+_; _*_; _∸_;_^_)

(Importing step-≡ defines _≡⟨_⟩_.)

运算符的性质

运算符随处可见,而数学家们统一了一些最常见的性质的名称。

  • 幺元(Identity):对于所有的 n,若 0 + n ≡ n,则 + 有左幺元 0; 若 n + 0 ≡ n,则 + 有右幺元 0。同时为左幺元和右幺元的值称简称幺元。 幺元有时也称作单位元(Unit)

  • 结合律(Associativity):若括号的位置无关紧要,则称运算符 + 满足结合律, 即对于所有的 mnp,有 (m + n) + p ≡ m + (n + p)

  • 交换律(Commutativity):若参数的顺序无关紧要,则称运算符 + 满足交换律, 即对于所有的 mn,有 m + n ≡ n + m

  • 分配律(Distributivity):对于所有的 mnp,若 m * (p + q) ≡ (m * p) + (m * q),则运算符 * 对运算符 + 满足左分配律; 对于所有的 mnp,若 (m + n) * p ≡ (m * p) + (n * p),则满足右分配律。

加法的幺元为 0,乘法的幺元为 1。加法和乘法都满足结合律和交换律, 乘法对加法满足分配律。

如果你在一个舞会上碰见了一位操作员,那么你可以跟他闲聊,问问他是否有单位元, 能不能结合或者交换。如果你碰见了两位操作员,那么可以问他们某一位是否在另一位上面分布。

【译注:作者的双关冷笑话,运算符(Operator)也有操作员的意思。】

正经来说,如果你在阅读技术论文时遇到了一个运算符,那么你可以考察它是否拥有幺元, 是否满足结合律或分配律,或者是否对另一个运算符满足分配律,这能为你提供一种视角。 细心的作者通常会指出它们是否满足这些性质,比如说指明一个新引入的运算符满足结合律 但不满足交换律。

练习 operators(实践)

请给出另一对运算符,它们拥有一个幺元,满足结合律、交换律,且其中一个对另一个满足分配律。 (你不必证明这些性质)

-- 请将代码写在此处。

请给出一个运算符的例子,它拥有幺元、满足结合律但不满足交换律。 (你不必证明这些性质)

-- 请将代码写在此处。

结合律

加法的一个性质是满足结合律,即括号的位置无关紧要:

(m + n) + p ≡ m + (n + p)

这里的变量 mnp 的取值范围都是全体自然数。

我们可以为这三个变量选取特定的数值来验证此命题:

_ : (3 + 4) + 5  3 + (4 + 5)
_ =
  begin
    (3 + 4) + 5
  ≡⟨⟩
    7 + 5
  ≡⟨⟩
    12
  ≡⟨⟩
    3 + 9
  ≡⟨⟩
    3 + (4 + 5)
  

在这里,我们将计算过程写成了等式链,每行一个式子。这样的等式链通常非常易读, 你可以从上到下,直到遇到最简形式(本例中为 12),也可以从下到上,直到回到同样的式子。

该测试揭示了结合律可能没有它初看起来那么显然。为什么 7 + 53 + 9 相同? 我们可能需要收集更多证据,选择其它的数值来验证此命题。但由于自然数是无限的, 因此测试永远无法完成。那么我们还有其它可以确保结合律对于所有自然数都成立的方法吗?

当然有!我们可以用归纳证明(Proof by Induction) 来确保某个性质对于所有的自然数都成立。

归纳证明

回想自然数的定义,它由一个起始步骤zero 是一个自然数」 和一个归纳步骤「若 m 是一个自然数,则 suc m 也是一个自然数」构成。

归纳证明遵循此定义的结构。要通过归纳证明自然数的某个性质,我们需要两个步骤。 其一是起始步骤,即需要证明此性质对 zero 成立。其二是归纳步骤, 即假设此性质对一个任意自然数 m 成立(我们称之为归纳假设(Induction Hypothesis)),然后证明该性质对 suc m 必定成立。

若将 m 的某种性质(Property)写作 P m,那么我们需要证明的就是以下两个推导规则:

------
P zero

P m
---------
P (suc m)

先来分析一下这些规则。第一条规则是起始步骤,它需要我们证明性质 Pzero 成立。第二条规则是归纳步骤,它需要我们证明若归纳假设「Pm 成立」, 那么 P 也对 suc m 成立。

为什么可以这样做呢?它也可以用创世故事来讲解。起初,我们对性质一无所知:

-- 起初,世上没有已知的性质。

现在我们对所有已知的性质应用上述两条规则。起始步骤告诉我们 P zero 成立, 所以我们将它加入已知的性质集合中。归纳步骤告诉我们若「昨天的」P m 成立, 那么「今天的」P (suc m) 也成立。我们在今天之前并不知道任何性质, 因此归纳步骤在这里不适用:

-- 第一天,我们知道了一个性质。
P zero

然后我们重复此过程。在接下来的一天我们知道今天之前的所有性质, 以及任何通过此规则添加的性质。起始步骤告诉我们 P zero 成立,我们已经知道这件事了。而如今归纳步骤告诉我们,由于 P zero 在昨天成立,那么 P (suc zero) 今天也成立。

-- 第二天,我们知道了两个性质。
P zero
P (suc zero)

我们再重复此过程。现在归纳步骤告诉我们由于 P zeroP (suc zero) 都成立, 因此 P (suc zero)P (suc (suc zero)) 也成立。我们已经知道第一个成立了, 但第二个是新引入的:

-- 第三天,我们知道了三个性质。
P zero
P (suc zero)
P (suc (suc zero))

此时规律已经很明显了:

-- 第四天,我们知道了四个性质。
P zero
P (suc zero)
P (suc (suc zero))
P (suc (suc (suc zero)))

此过程可以继续下去。在第 n 天会有 n 个不同的性质成立。 每个自然数的性质都会在某一天出现。具体来说,性质 P n 会在第 n+1 天 首次出现。

第一个证明:结合律

要证明结合律,我们需要将 P m 看做以下性质:

(m + n) + p ≡ m + (n + p)

这里的 np 是任意自然数,因此若我们可以证明该等式对所有的 m 都成立,那么它也会对所有的 np 成立。其推理规则的对应实例如下:

-------------------------------
(zero + n) + p ≡ zero + (n + p)

(m + n) + p ≡ m + (n + p)
---------------------------------
(suc m + n) + p ≡ suc m + (n + p)

如果我们可以证明这两条规则,那么加法结合律就可以用归纳法来证明。

以下为此性质的陈述和证明:

+-assoc :  (m n p : )  (m + n) + p  m + (n + p)
+-assoc zero n p =
  begin
    (zero + n) + p
  ≡⟨⟩
    n + p
  ≡⟨⟩
    zero + (n + p)
  
+-assoc (suc m) n p =
  begin
    (suc m + n) + p
  ≡⟨⟩
    suc (m + n) + p
  ≡⟨⟩
    suc ((m + n) + p)
  ≡⟨ cong suc (+-assoc m n p) 
    suc (m + (n + p))
  ≡⟨⟩
    suc m + (n + p)
  

我们将此证明命名为 +-assoc。在 Agda 中,标识符可以由除空格和 @.(){};_ 之外的任何字符序列构成。

我们来分析一下这段代码。其签名(Signature)描述了我们定义的标识符 +-assoc 为以下命题提供了证据(Evidence):

∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)

倒 A 符号读作「对于所有(for all)」,而该命题断言对于所有的自然数 mnp,等式 (m + n) + p ≡ m + (n + p) 成立。该命题的证据是一个接受三个自然数的函数, 将它们绑定到 mnp,并返回该等式对应实例的证据。

对于起始步骤,我们必须证明:

(zero + n) + p ≡ zero + (n + p)

用加法的起始步骤化简等式两边会得到:

n + p ≡ n + p

此式平凡成立。阅读此证明中起始步骤中的等式链,其最初和最末的式子分别匹配待证等式的两边, 从上到下或从下到上读都会让我们在中间遇到 n + p 。此步骤无需多言,化简即可。

对于归纳步骤,我们必须证明:

(suc m + n) + p ≡ suc m + (n + p)

用加法的归纳步骤化简等式两边会得到:

suc ((m + n) + p) ≡ suc (m + (n + p))

反之,它也可以通过在归纳假设

(m + n) + p ≡ m + (n + p)

两边之前加上 suc 得到。

阅读此证明中归纳步骤的等式链,其最初和最末的式子分别匹配待证等式的两边, 从上到下或从下到上读都会让我们到达上面化简等式的地方。剩下的等式单化简还不行, 我们还需要为推理链使用一个附加的运算符 _≡⟨_⟩_, 并将等式的依据放在尖括号中。这里给出的依据是:

⟨ cong suc (+-assoc m n p) ⟩

在这里,递归调用的 +-assoc m n p 拥有归纳假设的类型,而 cong suc 会在等式两边的前面加上 suc 以得到需要的等式。

若某个关系在应用了给定的函数后仍然保持不变,则称该关系满足合同性(Congruence)。 若 ex ≡ y 的证据,那么对于任意函数 fcong f e 就是 f x ≡ f y 的证据。

在这里并未假定归纳假设,而是通过递归调用我们定义的函数 +-assoc m n p 来证明。 对于加法,这是良基的(well-founded),因为更大数值的结合律可基于更小数值的结合律 来证明。在此步骤中,assoc (suc m) n p 是用 assoc m n p 证明的。 归纳证明和递归定义之间的这种对应是 Agda 中最吸引人的方面之一。

归纳即递归

下面是归纳如何对应于递归的具体例子,它是在结合律的证明中,将 m 实例化为 2 时的计算过程。

+-assoc-0 :  (n p : )  (0 + n) + p  0 + (n + p)
+-assoc-0 n p =
  begin
    (0 + n) + p
  ≡⟨⟩
    n + p
  ≡⟨⟩
    0 + (n + p)
  

+-assoc-1 :  (n p : )  (1 + n) + p  1 + (n + p)
+-assoc-1 n p =
  begin
    (1 + n) + p
  ≡⟨⟩
    suc (0 + n) + p
  ≡⟨⟩
    suc ((0 + n) + p)
  ≡⟨ cong suc (+-assoc-0 n p) 
    suc (0 + (n + p))
  ≡⟨⟩
    1 + (n + p)
  

+-assoc-2 :  (n p : )  (2 + n) + p  2 + (n + p)
+-assoc-2 n p =
  begin
    (2 + n) + p
  ≡⟨⟩
    suc (1 + n) + p
  ≡⟨⟩
    suc ((1 + n) + p)
  ≡⟨ cong suc (+-assoc-1 n p) 
    suc (1 + (n + p))
  ≡⟨⟩
    2 + (n + p)
  

术语与记法

在结合律的陈述中出现的符号 表示它对于所有的 mnp 都成立。 我们将 称为全称量词(Universal Quantifier),我们会在 Quantifiers 章节中进一步讨论。

全称量词的证据是一个函数。函数签名

+-assoc : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)

+-assoc : ∀ (m : ℕ) → ∀ (n : ℕ) → ∀ (p : ℕ) → (m + n) + p ≡ m + (n + p)

是等价的。和 ℕ → ℕ → ℕ 这样的函数类型不同,上述函数中的变量 与每一个实参类型相关联,且其结果类型可能会涉及(或依赖于)这些变量, 因此它们叫做依赖函数(Dependent Function)

Ordinary functions are a special case of dependent functions. For instance, the signatures

_+_ : ℕ → ℕ → ℕ

and

_+_ : ∀ (m n : ℕ) → ℕ

and

_+_ : ∀ (m : ℕ) → ∀ (n : ℕ) → ℕ

are all equivalent.

第二个证明:交换律

加法的另一个重要性质是满足交换律(Commutativity),即运算数的顺序无关紧要:

m + n ≡ n + m

要证明它,我们需要先证明两条引理(Lemma)。

第一条引理

加法定义的起始步骤说明零是一个左幺元:

zero + n ≡ n

我们的第一条引理则说明零也是一个右幺元:

m + zero ≡ m

下面是该引理的陈述和证明:

+-identityʳ :  (m : )  m + zero  m
+-identityʳ zero =
  begin
    zero + zero
  ≡⟨⟩
    zero
  
+-identityʳ (suc m) =
  begin
    suc m + zero
  ≡⟨⟩
    suc (m + zero)
  ≡⟨ cong suc (+-identityʳ m) 
    suc m
  

其签名说明我们定义的标识符 +-identityʳ 提供了以下命题的证据:

∀ (m : ℕ) → m + zero ≡ m

该命题的证据是一个函数,它接受一个自然数,将其绑定到 m,然后返回 该等式对应实例的证据。它通过对 m 进行归纳来证明。

对于起始步骤,我们必须证明:

zero + zero ≡ zero

根据加法的起始步骤化简,这很显然。

对于归纳步骤,我们必须证明:

(suc m) + zero = suc m

根据加法的归纳步骤化简等式两边可得:

suc (m + zero) = suc m

反之,它也可以通过在归纳假设

m + zero ≡ m

两边之前加上 suc 得到。

阅读此等式链,从上到下和从下到上读都会让我们到达上面化简等式的地方。 剩下的等式可由以下依据得出:

⟨ cong suc (+-identityʳ m) ⟩

在这里,递归调用的 +-identityʳ m 拥有归纳假设的类型,而 cong suc 会在等式两边的前面加上 suc 以得到需要的等式。第一条引理证毕。

第二条引理

加法定义的归纳步骤将第一个参数的 suc 推到了外面:

suc m + n ≡ suc (m + n)

我们的第二条引理则对第二个参数的 suc 做同样的事情:

m + suc n ≡ suc (m + n)

下面是该引理的陈述和证明:

+-suc :  (m n : )  m + suc n  suc (m + n)
+-suc zero n =
  begin
    zero + suc n
  ≡⟨⟩
    suc n
  ≡⟨⟩
    suc (zero + n)
  
+-suc (suc m) n =
  begin
    suc m + suc n
  ≡⟨⟩
    suc (m + suc n)
  ≡⟨ cong suc (+-suc m n) 
    suc (suc (m + n))
  ≡⟨⟩
    suc (suc m + n)
  

其签名说明我们定义的标识符 +-suc 提供了以下命题的证据:

∀ (m n : ℕ) → m + suc n ≡ suc (m + n)

该命题的证据是一个函数,它接受两个自然数,将二者分别绑定到 mn, 并返回该等式对应实例的证据。它通过对 m 进行归纳来证明。

对于起始步骤,我们必须证明:

zero + suc n ≡ suc (zero + n)

根据加法的起始步骤化简,这很显然。

对于归纳步骤,我们必须证明:

suc m + suc n ≡ suc (suc m + n)

根据加法的归纳步骤化简等式两边可得:

suc (m + suc n) ≡ suc (suc (m + n))

反之,它也可以通过在归纳假设

m + suc n ≡ suc (m + n)

两边之前加上 suc 得到。

从上到下或从下到上阅读等式链都会让我们在中间遇到化简后的等式。剩下的等式 可由以下依据得出:

⟨ cong suc (+-suc m n) ⟩

在这里,递归调用的 +-suc m n 拥有归纳假设的类型,而 cong suc 会在等式两边的前面加上 suc 以得到需要的等式。第二条引理证毕。

命题

最后,以下是我们的命题的陈述和证明:

+-comm :  (m n : )  m + n  n + m
+-comm m zero =
  begin
    m + zero
  ≡⟨ +-identityʳ m 
    m
  ≡⟨⟩
    zero + m
  
+-comm m (suc n) =
  begin
    m + suc n
  ≡⟨ +-suc m n 
    suc (m + n)
  ≡⟨ cong suc (+-comm m n) 
    suc (n + m)
  ≡⟨⟩
    suc n + m
  

第一行说明我们定义的标识符 +-comm 提供了以下命题的证据:

∀ (m n : ℕ) → m + n ≡ n + m

该命题的证据是一个函数,它接受两个自然数,将二者分别绑定到 mn, 并返回该等式对应实例的证据。它通过对 n 进行归纳来证明。(这次不是 m!)

对于起始步骤,我们必须证明:

m + zero ≡ zero + m

根据加法的起始步骤化简等式两边可得:

m + zero ≡ m

剩下的等式可由依据 ⟨ +-identityʳ m ⟩ 得出,它调用第一条引理。

对于归纳步骤,我们必须证明:

m + suc n ≡ suc n + m

根据加法的归纳步骤化简等式两边可得:

m + suc n ≡ suc (n + m)

我们分两步来证明它。首先,我们有:

m + suc n ≡ suc (m + n)

它依据第二条引理 ⟨ +-suc m n ⟩ 得出。之后我们有:

suc (m + n) ≡ suc (n + m)

它依据合同性和归纳假设 ⟨ cong suc (+-comm m n) ⟩ 得出。证毕。

Agda 要求标识符必须在使用前定义,因此我们必须在主命题之前列出引理, 如前例所示。在实践中,我们通常会先试着证明主命题,之后所需的等式会说明 需要证明哪些引理。

第一个推论:重排定理

我们可以随意应用结合律来重排括号。例如:

+-rearrange :  (m n p q : )  (m + n) + (p + q)  m + (n + p) + q
+-rearrange m n p q =
  begin
    (m + n) + (p + q)
  ≡⟨ sym (+-assoc (m + n) p q) 
    ((m + n) + p) + q
  ≡⟨ cong (_+ q) (+-assoc m n p) 
    (m + (n + p)) + q
  

无需归纳法,我们只不过应用了两次结合律就完成了证明。其中有几点需要注意的地方。

第一,加法是左结合的,因此 m + (n + p) + q 表示 (m + (n + p)) + q

第二,我们用 sym 来交换等式的两边。命题 +-assoc (m + n) p q 会将括号从左边移到右边:

((m + n) + p) + q ≡ (m + n) + (p + q)

要往另一个方向移动括号,我们要用 sym (+-assoc (m + n) p q)

(m + n) + (p + q) ≡ ((m + n) + p) + q

一般来说,若 e 提供了 x ≡ y 的证据,那么 sym e 就提供了 y ≡ x 的证据。

第三,Agda 支持 Richard Bird 引入的片段(Section)记法。我们将应用到 x 并返回 x + y 的函数写作 (_+ y)。因此,对于 assoc m n p 应用合同性 cong (_+ q) 会将等式:

(m + n) + p  ≡  m + (n + p)

转换成:

((m + n) + p) + q  ≡  (m + (n + p)) + q

类似地,我们将应用到 x 并返回 x + y 的函数写作 (x +_ )。 这同样适用于任何中缀运算符。

创世,最后一次

我们回到结合律的证明上来,把归纳证明(或等价的递归定义)看做一个创世故事会有助于理解。 这次我们专注于判断结合律的断言:

 -- 起初,我们对结合律一无所知。

现在,我们将规则应用到所有已知的判断上来。起始步骤告诉我们对于所有的自然数 np 来说,(zero + n) + p ≡ zero + (n + p)。归纳步骤告我我们若 (m + n) + p ≡ m + (n + p)(在昨天)成立,那么 (suc m + n) + p ≡ suc m + (n + p) (在今天)也成立。我们在今天之前并不知道任何关于结合律的判断, 因此此规则并未给出任何新的判断:

-- 第一天,我们知道了关于 0 的结合律。
(0 + 0) + 0 ≡ 0 + (0 + 0)   ...   (0 + 4) + 5 ≡ 0 + (4 + 5)   ...

之后我们重复此过程,因此接下来一天我们知道今天以前的所有判断, 以及任何通过此规则添加的判断。起始步骤并未告诉我们新的东西, 而如今归归纳步骤添加了更多的判断:

-- 第二天,我们知道了关于 0 和 1 的结合律。
(0 + 0) + 0 ≡ 0 + (0 + 0)   ...   (0 + 4) + 5 ≡ 0 + (4 + 5)   ...
(1 + 0) + 0 ≡ 1 + (0 + 0)   ...   (1 + 4) + 5 ≡ 1 + (4 + 5)   ...

我们再次重复此过程:

-- 第三天,我们知道了关于 0、1 和 2 的结合律。
(0 + 0) + 0 ≡ 0 + (0 + 0)   ...   (0 + 4) + 5 ≡ 0 + (4 + 5)   ...
(1 + 0) + 0 ≡ 1 + (0 + 0)   ...   (1 + 4) + 5 ≡ 1 + (4 + 5)   ...
(2 + 0) + 0 ≡ 2 + (0 + 0)   ...   (2 + 4) + 5 ≡ 2 + (4 + 5)   ...

此时规律已经很明显了:

-- 第四天,我们知道了关于 0、1、2 和 3 的结合律。
(0 + 0) + 0 ≡ 0 + (0 + 0)   ...   (0 + 4) + 5 ≡ 0 + (4 + 5)   ...
(1 + 0) + 0 ≡ 1 + (0 + 0)   ...   (1 + 4) + 5 ≡ 1 + (4 + 5)   ...
(2 + 0) + 0 ≡ 2 + (0 + 0)   ...   (2 + 4) + 5 ≡ 2 + (4 + 5)   ...
(3 + 0) + 0 ≡ 3 + (0 + 0)   ...   (3 + 4) + 5 ≡ 3 + (4 + 5)   ...

此过程可以继续下去。在第 m 天我们会知道所有第一个数小于 m 的判断。

还有一种完全有限的方法来生成同样的等式,它的证明留作读者的练习。

练习 finite-+-assoc(延伸)

请参考前文写出前四天已知的加法结合律的创世故事。

-- 请将代码写在此处。

用改写来证明结合律

证明可不止一种方法。下面是第二种在 Agda 中证明加法结合律的方法,使用 rewrite(改写) 而非等式链:

+-assoc′ :  (m n p : )  (m + n) + p  m + (n + p)
+-assoc′ zero    n p                          =  refl
+-assoc′ (suc m) n p  rewrite +-assoc′ m n p  =  refl

对于起始步骤,我们必须证明:

(zero + n) + p ≡ zero + (n + p)

根据加法的起始步骤化简等式两边可得:

n + p ≡ n + p

此式平凡成立。一个项等于其自身的证明写作 refl(自反性)。

对于归纳步骤,我们必须证明:

(suc m + n) + p ≡ suc m + (n + p)

根据加法的归纳步骤化简等式两边可得:

suc ((m + n) + p) ≡ suc (m + (n + p))

This is our goal to be proved. Rewriting by a given equation is indicated by the keyword rewrite followed by a proof of that equation. Rewriting replaces each occurrence of the left-hand side of the equation in the goal by the right-hand side. In this case, after rewriting by the inductive hypothesis our goal becomes

suc (m + (n + p)) ≡ suc (m + (n + p))

其证明同样由 refl 给出。改写不仅可以省去等式链还可以避免调用 cong.

使用改写证明交换律

下面是加法交换律的第二个证明,使用 rewrite 而非等式链:

+-identity′ :  (n : )  n + zero  n
+-identity′ zero = refl
+-identity′ (suc n) rewrite +-identity′ n = refl

+-suc′ :  (m n : )  m + suc n  suc (m + n)
+-suc′ zero n = refl
+-suc′ (suc m) n rewrite +-suc′ m n = refl

+-comm′ :  (m n : )  m + n  n + m
+-comm′ m zero rewrite +-identity′ m = refl
+-comm′ m (suc n) rewrite +-suc′ m n | +-comm′ m n = refl

在最后一行中,用两个等式进行改写被表示为用一条竖线分隔两个相关等式的证明。 左边的改写会在右边之前被执行。

交互式构造证明

看看如何在 Emacs 中用 Agda 的交互式特性来构造另一种结合律的证明会很有启发性。 我们从输入以下内容开始:

+-assoc′ : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)
+-assoc′ m n p = ?

其中的问号表示你想要 Agda 帮你填充的代码。如果你按下 C-c C-l (先按 Ctrl-c 再按 Ctrl-l),那么问号会被替换为:

+-assoc′ : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)
+-assoc′ m n p = { }0

空的大括号叫做洞(Hole),0 是用来指代此洞的编号。洞可能会以绿色高亮显示。 Emacs 还会在屏幕下方创建一个新的窗口并显示文本:

?0 : ((m + n) + p) ≡ (m + (n + p))

这表示 0 号洞需要以所提示的判断的证明来填充。

我们希望对 m 进行归纳来证明此命题。将光标移动到洞中并按下 C-c C-c。它会给出提示:

pattern variables to case (empty for split on result):

按下 m 会拆分该变量,并更新此代码:

+-assoc′ : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)
+-assoc′ zero n p = { }0
+-assoc′ (suc m) n p = { }1

现在有两个洞了,下方的窗口会告诉你每个洞中需要证明的内容:

?0 : ((zero + n) + p) ≡ (zero + (n + p))
?1 : ((suc m + n) + p) ≡ (suc m + (n + p))

进入 0 号洞并按下 C-c C-, 会显示以下文本:

Goal: (n + p) ≡ (n + p)
————————————————————————————————————————————————————————————
p : ℕ
n : ℕ

它表示在化简之后,0 号洞的目标如上所示,所示类型的变量 pn 可在证明中使用。 给定目标的证明很平凡,只需进入该目标并按下 C-c C-r 即可填充。按下 C-c C-l 会将剩下的洞重新编号为 0:

+-assoc′ : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)
+-assoc′ zero n p = refl
+-assoc′ (suc m) n p = { }0

进入新的 0 号洞并按下 C-c C-, 会显示以下文本:

Goal: suc ((m + n) + p) ≡ suc (m + (n + p))
————————————————————————————————————————————————————————————
p : ℕ
n : ℕ
m : ℕ

同样,它会给出化简后的目标和可用的变量。在此步骤中,我们需要根据归纳假设进行改写, 于是我们来编辑这些文本:

+-assoc′ : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)
+-assoc′ zero n p = refl
+-assoc′ (suc m) n p rewrite +-assoc′ m n p = { }0

进入剩下的洞中并按下 C-c C-, 会显示以下文本:

Goal: suc (m + (n + p)) ≡ suc (m + (n + p))
————————————————————————————————————————————————————————————
p : ℕ
n : ℕ
m : ℕ

给定目标的证明很平凡,只需进入该目标并按下 C-c C-r 即可填充并完成证明:

+-assoc′ : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)
+-assoc′ zero n p = refl
+-assoc′ (suc m) n p rewrite +-assoc′ m n p = refl
练习:+-swap(推荐)

请证明对于所有的自然数 mnp

m + (n + p) ≡ n + (m + p)

成立。无需归纳证明,只需应用前面满足结合律和交换律的结果即可。

-- 请将代码写在此处。
练习 *-distrib-+(推荐)

请证明乘法对加法满足分配律,即对于所有的自然数 mnp

(m + n) * p ≡ m * p + n * p

成立。

-- 请将代码写在此处。
练习 *-assoc(推荐)

请证明乘法满足结合律,即对于所有的自然数 mnp

(m * n) * p ≡ m * (n * p)

成立。

-- 请将代码写在此处。
练习 *-comm(实践)

请证明乘法满足交换律,即对于所有的自然数 mn

m * n ≡ n * m

成立。和加法交换律一样,你需要陈述并证明配套的引理。

-- 请将代码写在此处。
练习 0∸n≡0(实践)

请证明对于所有的自然数 n

zero ∸ n ≡ zero

成立。你的证明需要归纳法吗?

-- 请将代码写在此处。
练习 ∸-+-assoc(实践)

请证明饱和减法与加法满足结合律,即对于所有的自然数 mnp

m ∸ n ∸ p ≡ m ∸ (n + p)

成立。

-- 请将代码写在此处。
练习 +*^ (延伸)

证明下列三条定律

 m ^ (n + p) ≡ (m ^ n) * (m ^ p)  (^-distribˡ-+-*)
 (m * n) ^ p ≡ (m ^ p) * (n ^ p)  (^-distribʳ-*)
 (m ^ n) ^ p ≡ m ^ (n * p)        (^-*-assoc)

对于所有 mnp 成立。

-- 请将代码写在此处。
练习 Bin-laws(延伸)

回想练习 Bin 中定义的一种表示自然数的比特串数据类型 Bin 以及要求你定义的函数:

inc   : Bin → Bin
to    : ℕ → Bin
from  : Bin → ℕ

考虑以下定律,其中 n 表示自然数,b 表示比特串:

from (inc b) ≡ suc (from b)
to (from b) ≡ b
from (to n) ≡ n

对于每一条定律:若它成立,请证明;若不成立,请给出一个反例。

-- 请将代码写在此处。

标准库

本章中类似的定义可在标准库中找到:

import Data.Nat.Properties using (+-assoc; +-identityʳ; +-suc; +-comm)

Unicode

本章中使用了以下 Unicode:

∀  U+2200  对于所有 (\forall, \all)
ʳ  U+02B3  修饰符小写字母 r (\^r)
′  U+2032  撇号 (\')
″  U+2033  双撇号 (\')
‴  U+2034  三撇号 (\')
⁗  U+2057  四撇号 (\')

\r 类似,命令 \^r 列出了多种上标右箭头的变体,以及上标的字母 r。 命令 \' 列出了一些撇号(′ ″ ‴ ⁗)。

Relations: 关系的归纳定义

module plfa.part1.Relations where

在定义了加法和乘法等运算以后,下一步我们来定义关系(Relation),比如说小于等于

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl; cong)
open import Data.Nat using (; zero; suc; _+_)
open import Data.Nat.Properties using (+-comm; +-identityʳ)

定义关系

小于等于这个关系有无穷个实例,如下所示:

0 ≤ 0     0 ≤ 1     0 ≤ 2     0 ≤ 3     ...
          1 ≤ 1     1 ≤ 2     1 ≤ 3     ...
                    2 ≤ 2     2 ≤ 3     ...
                              3 ≤ 3     ...
                                        ...

但是,我们仍然可以用几行有限的定义来表示所有的实例,如下文所示的一对推理规则:

z≤n --------
    zero ≤ n

    m ≤ n
s≤s -------------
    suc m ≤ suc n

以及其 Agda 定义:

data _≤_ :     Set where

  z≤n :  {n : }
      --------
     zero  n

  s≤s :  {m n : }
     m  n
      -------------
     suc m  suc n

在这里,z≤ns≤s(无空格)是构造子的名称,zero ≤ nm ≤ nsuc m ≤ suc n (带空格)是类型。在这里我们第一次用到了 索引数据类型(Indexed datatype)。我们使用 mn 这两个自然数来索引 m ≤ n 这个类型。在 Agda 里,由两个及以上短横线开始的行是注释行, 我们巧妙利用这一语法特性,用上述形式来表示相应的推理规则。 在后文中,我们还会继续使用这一形式。

这两条定义告诉我们相同的两件事:

  • 起始步骤: 对于所有的自然数 n,命题 zero ≤ n 成立。
  • 归纳步骤:对于所有的自然数 mn,如果命题 m ≤ n 成立, 那么命题 suc m ≤ suc n 成立。

实际上,他们分别给我们更多的信息:

  • 起始步骤: 对于所有的自然数 n,构造子 z≤n 提供了 zero ≤ n 成立的证明。
  • 归纳步骤:对于所有的自然数 mn,构造子 s≤sm ≤ n 成立的证明 转化为 suc m ≤ suc n 成立的证明。

例如,我们在这里以推理规则的形式写出 2 ≤ 4 的证明:

  z≤n -----
      0 ≤ 2
 s≤s -------
      1 ≤ 3
s≤s ---------
      2 ≤ 4

下面是对应的 Agda 证明:

_ : 2  4
_ = s≤s (s≤s z≤n)

隐式参数

这是我们第一次使用隐式参数。定义不等式时,构造子的定义中使用了 , 就像我们在下面的命题中使用 一样:

+-comm : ∀ (m n : ℕ) → m + n ≡ n + m

但是我们这里的定义使用了花括号 { },而不是小括号 ( )。 这意味着参数是隐式的(Implicit),不需要额外声明。实际上,Agda 的类型检查器 会推导(Infer)出它们。因此,我们在 m + n ≡ n + m 的证明中需要写出 +-comm m n, 在 zero ≤ n 的证明中可以省略 n。同理,如果 m≤nm ≤ n的证明, 那么我们写出 s≤s m≤n 作为 suc m ≤ suc n 的证明,无需声明 mn

如果有希望的话,我们也可以在大括号里显式声明隐式参数。例如,下面是 2 ≤ 4 的 Agda 证明,包括了显式声明了的隐式参数:

_ : 2  4
_ = s≤s {1} {3} (s≤s {0} {2} (z≤n {2}))

也可以额外加上参数的名字:

_ : 2  4
_ = s≤s {m = 1} {n = 3} (s≤s {m = 0} {n = 2} (z≤n {n = 2}))

在后者的形式中,也可以选择只声明一部分隐式参数:

_ : 2  4
_ = s≤s {n = 3} (s≤s {n = 2} z≤n)

但是不可以改变隐式参数的顺序,即便加上了名字。

我们可以写出 _ 来让 Agda 用相同的推导方式试着推导一个显式的项。 例如,我们可以为命题 +-identityʳ 定义一个带有隐式参数的变体:

+-identityʳ′ :  {m : }  m + zero  m
+-identityʳ′ = +-identityʳ _

我们用 _ 来让 Agda 从语境中推导显式参数的值。只有 m 这一个值能够给出正确的证明,因此 Agda 愉快地填入了它。 如果 Agda 推导值失败,那么它会报一个错误。

优先级

我们如下定义比较的优先级:

infix 4 _≤_

我们将 _≤_ 的优先级设置为 4,所以它不如优先级为 6 的 _+_ 结合得紧,因而, 1 + 2 ≤ 3 将被解析为 (1 + 2) ≤ 3。我们用 infix 来表示运算符既不是左结合的, 也不是右结合的。因为 1 ≤ 2 ≤ 3 解析为 (1 ≤ 2) ≤ 3 或者 1 ≤ (2 ≤ 3) 都没有意义。

可判定性

给定两个数,我们可以很直接地决定第一个数是不是小于等于第二个数。我们在此处不给出说明的代码, 但我们会在 Decidable 章节重新讨论这个问题。

反演

在我们的定义中,我们从更小的东西得到更大的东西。例如,我们可以从 m ≤ n 得出 suc m ≤ suc n 的结论,这里的 suc mm 更大 (也就是说,前者包含后者),suc n 也比 n 更大。但有时我们也 需要从更大的东西得到更小的东西。

只有一种方式能够证明对于任意 mnsuc m ≤ suc n。 这让我们能够反演(invert)之前的规则。

inv-s≤s :  {m n : }
   suc m  suc n
    -------------
   m  n
inv-s≤s (s≤s m≤n) = m≤n

这里的 m≤n(不带空格)是一个变量名,而 m ≤ n(带空格)是一个类型, 且后者是前者的类型。在 Agda 中,将类型中的空格去掉来作为变量名是一种常见的约定。

并不是所有规则都可以反演。实际上,z≤n 的规则没有非隐式的假设, 因此它没有可以被反演的规则。但这种反演通常是成立的。

反演的另一个例子是证明只存在一种情况使得一个数字能够小于或等于零。

inv-z≤n :  {m : }
   m  zero
    --------
   m  zero
inv-z≤n z≤n = refl

序关系的性质

数学家对于关系的常见性质给出了约定的名称。

  • 自反(Reflexive):对于所有的 n,关系 n ≤ n 成立。
  • 传递(Transitive):对于所有的 mnp,如果 m ≤ nn ≤ p 成立,那么 m ≤ p 也成立。
  • 反对称(Anti-symmetric):对于所有的 mn,如果 m ≤ nn ≤ m 同时成立,那么 m ≡ n 成立。
  • 完全(Total):对于所有的 mnm ≤ n 或者 n ≤ m 成立。

_≤_ 关系满足上述四条性质。

对于上述性质的组合也有约定的名称。

  • 预序(Preorder):满足自反和传递的关系。
  • 偏序(Partial Order):满足反对称的预序。
  • 全序(Total Order):满足完全的偏序。

如果你在派对上偶遇一个『关系』,你现在知道怎么样和人讨论了, 可以讨论关于自反、传递、反对称和完全, 或者问一问这是不是预序、偏序或者全序。

更认真的来说,如果你在阅读论文时碰到了一个关系,本文的介绍让你可以对关系有基本的了解和判断, 来判断这个关系是不是预序、偏序或者全序。一个认真的作者一般会在文章指出这个关系具有(或者缺少) 上述性质,比如说指出新定义的关系是一个偏序而不是全序。

练习 orderings(实践)

给出一个不是偏序的预序的例子。

-- 请将代码写在此处。

给出一个不是全序的偏序的例子。

-- 请将代码写在此处。

自反性

我们第一个来证明的性质是自反性:对于任意自然数 n,关系 n ≤ n 成立。我们使用标准库 的惯例来隐式申明参数,在使用自反性的证明时这样可以更加方便。

≤-refl :  {n : }
    -----
   n  n
≤-refl {zero} = z≤n
≤-refl {suc n} = s≤s ≤-refl

这个证明直接在 n 上进行归纳。在起始步骤中,zero ≤ zeroz≤n 证明;在归纳步骤中, 归纳假设 ≤-refl {n} 给我们带来了 n ≤ n 的证明,我们只需要使用 s≤s,就可以获得 suc n ≤ suc n 的证明。

在 Emacs 中来交互式地证明自反性是一个很好的练习,可以使用洞,以及 C-c C-cC-c C-,C-c C-r 命令。

传递性

我们第二个证明的性质是传递性:对于任意自然数 mn,如果 m ≤ nn ≤ p 成立,那么 m ≤ p 成立。同样,mnp 是隐式参数:

≤-trans :  {m n p : }
   m  n
   n  p
    -----
   m  p
≤-trans z≤n       _          =  z≤n
≤-trans (s≤s m≤n) (s≤s n≤p)  =  s≤s (≤-trans m≤n n≤p)

这里我们在 m ≤ n证据(Evidence)上进行归纳。在起始步骤里,第一个不等式因为 z≤n 而成立, 那么结论亦可由 z≤n 而得出。在这里,n ≤ p 的证明是不需要的,我们用 _ 来表示这个 证明没有被使用。

在归纳步骤中,第一个不等式因为 s≤s m≤n 而成立,第二个不等式因为 s≤s n≤p 而成立, 所以我们已知 suc m ≤ suc nsuc n ≤ suc p,求证 suc m ≤ suc p。 通过归纳假设 ≤-trans m≤n n≤p,我们得知 m ≤ p,在此之上使用 s≤s 即可证。

≤-trans (s≤s m≤n) z≤n 不可能发生,因为第一个不等式告诉我们中间的数是一个 suc n, 而第二个不等式告诉我们中间的数是 zero。Agda 可以推断这样的情况不可能发现,所以我们不需要 (也不可以)列出这种情况。

我们也可以将隐式参数显式地声明。

≤-trans′ :  (m n p : )
   m  n
   n  p
    -----
   m  p
≤-trans′ zero    _       _       z≤n       _          =  z≤n
≤-trans′ (suc m) (suc n) (suc p) (s≤s m≤n) (s≤s n≤p)  =  s≤s (≤-trans′ m n p m≤n n≤p)

有人说这样的证明更加的清晰,也有人说这个更长的证明让人难以抓住证明的重点。 我们一般选择使用简短的证明。

对于性质成立证明进行的归纳(如上文中对于 m ≤ n 的证明进行归纳),相比于对于性质成立的值进行的归纳 (如对于 m 进行归纳),有非常大的价值。我们会经常使用这样的方法。

同样,在 Emacs 中来交互式地证明传递性是一个很好的练习,可以使用洞,以及 C-c C-cC-c C-,C-c C-r 命令。

反对称性

我们证明的第三个性质是反对称性:对于所有的自然数 mn,如果 m ≤ nn ≤ m 同时成立,那么 m ≡ n 成立:

≤-antisym :  {m n : }
   m  n
   n  m
    -----
   m  n
≤-antisym z≤n       z≤n        =  refl
≤-antisym (s≤s m≤n) (s≤s n≤m)  =  cong suc (≤-antisym m≤n n≤m)

同样,我们对于 m ≤ nn ≤ m 的证明进行归纳。

在起始步骤中,两个不等式都因为 z≤n 而成立。因此我们已知 zero ≤ zerozero ≤ zero, 求证 zero ≡ zero,由自反性可证。(注:由等式的自反性可证,而不是不等式的自反性)

在归纳步骤中,第一个不等式因为 s≤s m≤n 而成立,第二个等式因为 s≤s n≤m 而成立。因此我们已知 suc m ≤ suc nsuc n ≤ suc m,求证 suc m ≡ suc n。归纳假设 ≤-antisym m≤n n≤m 可以证明 m ≡ n,因此我们可以使用同余性完成证明。

练习 ≤-antisym-cases(实践)

上面的证明中省略了一个参数是 z≤n,另一个参数是 s≤s 的情况。为什么可以省略这种情况?

-- 请将代码写在此处。

完全性

我们证明的第四个性质是完全性:对于任何自然数 mnm ≤ n 或者 n ≤ m 成立。 在 mn 相等时,两者同时成立。

我们首先来说明怎么样不等式才是完全的:

data Total (m n : ) : Set where

  forward :
      m  n
      ---------
     Total m n

  flipped :
      n  m
      ---------
     Total m n

Total m n 成立的证明有两种形式:forward m≤n 或者 flipped n≤m,其中 m≤nn≤m 分别是 m ≤ nn ≤ m 的证明。

(如果你对于逻辑学有所了解,上面的定义可以由析取(Disjunction)表示。 我们会在 Connectives 章节介绍析取。)

这是我们第一次使用带参数的数据类型,这里 mn 是参数。这等同于下面的索引数据类型:

data Total′ :     Set where

  forward′ :  {m n : }
     m  n
      ----------
     Total′ m n

  flipped′ :  {m n : }
     n  m
      ----------
     Total′ m n

类型里的每个参数都转换成构造子的一个隐式参数。索引数据类型中的索引可以变化,正如在 zero ≤ nsuc m ≤ suc n 中那样,而参数化数据类型不一样,其参数必须保持相同, 正如在 Total m n 中那样。参数化的声明更短,更易于阅读,而且有时可以帮助到 Agda 的 停机检查器,所以我们尽可能地使用它们,而不是索引数据类型。

在上述准备工作完成后,我们定义并证明完全性。

≤-total :  (m n : )  Total m n
≤-total zero    n                         =  forward z≤n
≤-total (suc m) zero                      =  flipped z≤n
≤-total (suc m) (suc n) with ≤-total m n
...                        | forward m≤n  =  forward (s≤s m≤n)
...                        | flipped n≤m  =  flipped (s≤s n≤m)

这里,我们的证明在两个参数上进行归纳,并按照情况分析:

  • 第一起始步骤:如果第一个参数是 zero,第二个参数是 n,那么 forward 条件成立,我们使用 z≤n 作为 zero ≤ n 的证明。

  • 第二起始步骤:如果第一个参数是 suc m,第二个参数是 zero,那么 flipped 条件成立,我们使用 z≤n 作为 zero ≤ suc m 的证明。

  • 归纳步骤:如果第一个参数是 suc m,第二个参数是 suc n,那么归纳假设 ≤-total m n 可以给出如下推断:

    • 归纳假设的 forward 条件成立,以 m≤n 作为 m ≤ n 的证明。以此我们可以使用 s≤s m≤n 作为 suc m ≤ suc n 来证明 forward 条件成立。

    • 归纳假设的 flipped 条件成立,以 n≤m 作为 n ≤ m 的证明。以此我们可以使用 s≤s n≤m 作为 suc n ≤ suc m 来证明 flipped 条件成立。

这是我们第一次在 Agda 中使用 with 语句。with 关键字后面有一个表达式和一或多行。 每行以省略号(...)和一个竖线(|)开头,后面跟着用来匹配表达式的模式,和等式的右手边。

使用 with 语句等同于定义一个辅助函数。比如说,上面的定义和下面的等价:

≤-total′ :  (m n : )  Total m n
≤-total′ zero    n        =  forward z≤n
≤-total′ (suc m) zero     =  flipped z≤n
≤-total′ (suc m) (suc n)  =  helper (≤-total′ m n)
  where
  helper : Total m n  Total (suc m) (suc n)
  helper (forward m≤n)  =  forward (s≤s m≤n)
  helper (flipped n≤m)  =  flipped (s≤s n≤m)

这也是我们第一次在 Agda 中使用 where 语句。where 关键字后面有一或多条定义,其必须被缩进。 之前等式左手边的约束变量(此例中的 mn)在嵌套的定义中仍然在作用域内。 在嵌套定义中的约束标识符(此例中的 helper )在等式的右手边的作用域内。

如果两个参数相同,那么两个情况同时成立,我们可以返回任一证明。上面的代码中我们返回 forward 条件, 但是我们也可以返回 flipped 条件,如下:

≤-total″ :  (m n : )  Total m n
≤-total″ m       zero                      =  flipped z≤n
≤-total″ zero    (suc n)                   =  forward z≤n
≤-total″ (suc m) (suc n) with ≤-total″ m n
...                         | forward m≤n  =  forward (s≤s m≤n)
...                         | flipped n≤m  =  flipped (s≤s n≤m)

两者的区别在于上述代码在对于第一个参数进行模式匹配之前先对于第二个参数先进行模式匹配。

单调性

如果在聚会中碰到了一个运算符和一个序,那么有人可能会问这个运算符对于这个序是不是 单调的(Monotonic)。比如说,加法对于小于等于是单调的,这意味着:

∀ {m n p q : ℕ} → m ≤ n → p ≤ q → m + p ≤ n + q

这个证明可以用我们学会的方法,很直接的来完成。我们最好把它分成三个部分,首先我们证明加法对于 小于等于在右手边是单调的:

+-monoʳ-≤ :  (n p q : )
   p  q
    -------------
   n + p  n + q
+-monoʳ-≤ zero    p q p≤q  =  p≤q
+-monoʳ-≤ (suc n) p q p≤q  =  s≤s (+-monoʳ-≤ n p q p≤q)

我们对于第一个参数进行归纳。

  • 起始步骤:第一个参数是 zero,那么 zero + p ≤ zero + q 可以化简为 p ≤ q, 其证明由 p≤q 给出。

  • 归纳步骤:第一个参数是 suc n,那么 suc n + p ≤ suc n + q 可以化简为 suc (n + p) ≤ suc (n + q)。归纳假设 +-monoʳ-≤ n p q p≤q 可以证明 n + p ≤ n + q,我们在此之上使用 s≤s 即可得证。

接下来,我们证明加法对于小于等于在左手边是单调的。我们可以用之前的结论和加法的交换律来证明:

+-monoˡ-≤ :  (m n p : )
   m  n
    -------------
   m + p  n + p
+-monoˡ-≤ m n p m≤n  rewrite +-comm m p | +-comm n p  = +-monoʳ-≤ p m n m≤n

+-comm m p+-comm n p 来重写,可以让 m + p ≤ n + p 转换成 p + n ≤ p + m, 而我们可以用 +-moroʳ-≤ p m n m≤n 来证明。

最后,我们把前两步的结论结合起来:

+-mono-≤ :  (m n p q : )
   m  n
   p  q
    -------------
   m + p  n + q
+-mono-≤ m n p q m≤n p≤q  =  ≤-trans (+-monoˡ-≤ m n p m≤n) (+-monoʳ-≤ n p q p≤q)

使用 +-monoˡ-≤ m n p m≤n 可以证明 m + p ≤ n + p, 使用 +-monoʳ-≤ n p q p≤q 可以证明 n + p ≤ n + q,用传递性把两者连接起来, 我们可以获得 m + p ≤ n + q 的证明,如上所示。

练习 *-mono-≤ (延伸)

证明乘法对于小于等于是单调的。

-- 请将代码写在此处。

严格不等关系

我们可以用类似于定义不等关系的方法来定义严格不等关系。

infix 4 _<_

data _<_ :     Set where

  z<s :  {n : }
      ------------
     zero < suc n

  s<s :  {m n : }
     m < n
      -------------
     suc m < suc n

严格不等关系与不等关系最重要的区别在于,0 小于任何数的后继,而不小于 0。

显然,严格不等关系不是自反的,而是非自反的(Irreflexive),表示 n < n 对于 任何值 n 都不成立。和不等关系一样,严格不等关系是传递的。严格不等关系不是完全的,但是满足 一个相似的性质:三分律(Trichotomy):对于任意的 mnm < nm ≡ n 或者 m > n 三者有且仅有一者成立。(我们定义 m > n 当且仅当 n < m 成立时成立) 严格不等关系对于加法和乘法也是单调的。

我们把一部分上述性质作为习题。非自反性需要逻辑非,三分律需要证明三者是互斥的,因此这两个性质 暂不做为习题。我们会在 Negation 章节来重新讨论。

我们可以直接地来证明 suc m ≤ n 蕴涵了 m < n,及其逆命题。 因此我们亦可从不等关系的性质中,使用此性质来证明严格不等关系的性质。

练习 <-trans (推荐)

证明严格不等是传递的。请直接证明。(后续的练习中我们将使用 < 和 ≤ 的关系。)

-- 请将代码写在此处。
练习 trichotomy(实践)

证明严格不等关系满足弱化的三元律,证明对于任意 mn,下列命题有一条成立:

  • m < n
  • m ≡ n,或者
  • m > n

定义 m > nn < m。你需要一个合适的数据类型声明,如同我们在证明完全性中使用的那样。 (我们会在介绍完否定之后证明三者是互斥的。)

-- 请将代码写在此处。
练习 +-mono-<(实践)

证明加法对于严格不等关系是单调的。正如不等关系中那样,你可以需要额外的定义。

-- 请将代码写在此处。
练习 ≤-iff-< (推荐)

证明 suc m ≤ n 蕴涵了 m < n,及其逆命题。

-- 请将代码写在此处。
练习 <-trans-revisited(实践)

用另外一种方法证明严格不等是传递的,使用之前证明的不等关系和严格不等关系的联系, 以及不等关系的传递性。

-- 请将代码写在此处。

奇和偶

作为一个额外的例子,我们来定义奇数和偶数。不等关系和严格不等关系是二元关系,而奇偶性 是一元关系,有时也被叫做谓词(Predicate)

data even :   Set
data odd  :   Set

data even where

  zero :
      ---------
      even zero

  suc  :  {n : }
     odd n
      ------------
     even (suc n)

data odd where

  suc  :  {n : }
     even n
      -----------
     odd (suc n)

一个数是偶数,如果它是 0,或者是奇数的后继。一个数是奇数,如果它是偶数的后继。

这是我们第一次定义一个相互递归的数据类型。因为每个标识符必须在使用前声明,所以 我们首先声明索引数据类型 evenodd (省略 where 关键字和其构造子的定义), 然后声明其构造子(省略其签名 ℕ → Set,因为在之前已经给出)。

这也是我们第一次使用重载(Overloaded)的构造子。这意味着不同类型的构造子 拥有相同的名字。在这里 suc 表示下面三种构造子其中之一:

suc : ℕ → ℕ

suc : ∀ {n : ℕ}
  → odd n
    ------------
  → even (suc n)

suc : ∀ {n : ℕ}
  → even n
    -----------
  → odd (suc n)

同理,zero 表示两种构造子的一种。因为类型推导的限制,Agda 不允许重载已定义的名字, 但是允许重载构造子。我们推荐将重载限制在有关联的定义中,如我们所做的这样,但这不是必须的。

我们证明两个偶数之和是偶数:

e+e≡e :  {m n : }
   even m
   even n
    ------------
   even (m + n)

o+e≡o :  {m n : }
   odd m
   even n
    -----------
   odd (m + n)

e+e≡e zero     en  =  en
e+e≡e (suc om) en  =  suc (o+e≡o om en)

o+e≡o (suc em) en  =  suc (e+e≡e em en)

与相互递归的定义对应,我们用两个相互递归的函数,一个证明两个偶数之和是偶数,另一个证明 一个奇数与一个偶数之和是奇数。

这是我们第一次使用相互递归的函数。因为每个标识符必须在使用前声明,我们先给出两个函数的签名, 然后再给出其定义。

要证明两个偶数之和为偶,我们考虑第一个数为偶数的证明。如果是因为第一个数为 0, 那么第二个数为偶数的证明即为和为偶数的证明。如果是因为第一个数为奇数的后继, 那么和为偶数是因为他是一个奇数和一个偶数的和的后续,而这个和是一个奇数。

要证明一个奇数和一个偶数的和是奇数,我们考虑第一个数是奇数的证明。 如果是因为它是一个偶数的后继,那么和为奇数,因为它是两个偶数之和的后继, 而这个和是一个偶数。

练习 o+o≡e (延伸)

证明两个奇数之和为偶数。

-- 请将代码写在此处。
练习 Bin-predicates (延伸)

回忆我们在练习 Bin 中定义了一个数据类型 Bin 来用二进制字符串表示自然数。 这个表达方法不是唯一的,因为我们在开头加任意个 0。因此,11 可以由以下方法表示:

⟨⟩ I O I I
⟨⟩ O O I O I I

定义一个谓词

Can : Bin → Set

其在一个二进制字符串的表示是标准的(Canonical)时成立,表示它没有开头的 0。在两个 11 的表达方式中, 第一个是标准的,而第二个不是。在定义这个谓词时,你需要一个辅助谓词:

One : Bin → Set

其仅在一个二进制字符串开头为 1 时成立。一个二进制字符串是标准的,如果它开头是 1 (表示一个正数), 或者它仅是一个 0 (表示 0)。

证明递增可以保持标准性。

Can b
------------
Can (inc b)

证明从自然数转换成的二进制字符串是标准的。

----------
Can (to n)

证明将一个标准的二进制字符串转换成自然数之后,再转换回二进制字符串与原二进制字符串相同。

Can b
---------------
to (from b) ≡ b

提示:对于每一条习题,先从 One 的性质开始。 证明以下命题也会很有帮助。

One b
----------
1 ≤ from b

1 ≤ n
---------------------
to (2 * n) ≡ (to n) O
-- 请将代码写在此处。

标准库

标准库中有类似于本章介绍的定义:

import Data.Nat using (_≤_; z≤n; s≤s)
import Data.Nat.Properties using (≤-refl; ≤-trans; ≤-antisym; ≤-total;
                                  +-monoʳ-≤; +-monoˡ-≤; +-mono-≤)

在标准库中,≤-total 是使用析取定义的(我们将在 Connectives 章节定义)。 +-monoʳ-≤+-monoˡ-≤+-mono-≤ 的证明方法和本书不同。 更多的参数是隐式申明的。

Unicode

本章使用了如下 Unicode 符号:

≤  U+2264  小于等于 (\<=, \le)
≥  U+2265  大于等于 (\>=, \ge)
ˡ  U+02E1  小写字母 L 标识符 (\^l)
ʳ  U+02B3  小写字母 R 标识符 (\^r)

\^l\^r 命令给出了左右箭头,以及上标字母 lr

Equality: 相等性与等式推理

module plfa.part1.Equality where

我们在论证的过程中经常会使用相等性。给定两个都为 A 类型的项 MN, 我们用 M ≡ N 来表示 MN 可以相互替换。在此之前, 我们将相等性作为一个基础运算,而现在我们来说明如何将其定义为一个归纳的数据类型。

导入

本章节没有导入的内容。本书的每一章节,以及 Agda 标准库的每个模块都导入了相等性。 我们在此定义相等性,导入其他内容将会产生冲突。

相等性

我们如下定义相等性:
data _≡_ {A : Set} (x : A) : A  Set where
  refl : x  x

用其他的话来说,对于任意类型 A 和任意 A 类型的 x,构造子 refl 提供了 x ≡ x 的证明。所以,每个值等同于它本身,我们并没有其他办法来证明值的相等性。 这个定义里有不对称的地方,_≡_ 的第一个参数(Argument)由 x : A 给出, 而第二个参数(Argument)则是由 A → Set 的索引给出。 这和我们尽可能多的使用参数(Parameter)的理念相符。_≡_ 的第一个参数(Argument) 可以作为一个参数(Parameter),因为它不会变,而第二个参数(Argument)则必须是一个索引, 这样它才可以等于第一个。

我们如下定义相等性的优先级:

infix 4 _≡_

我们将 _≡_ 的优先级设置为 4,与 _≤_ 相同,所以其它算术运算符的结合都比它紧密。 由于它既不是左结合,也不是右结合的,因此 x ≡ y ≡ z 是不合法的。

相等性是一个等价关系(Equivalence Relation)

一个等价关系是自反、对称和传递的。其中自反性可以通过构造子 refl 直接从相等性的定义中得来。 我们可以直接地证明其对称性:

sym :  {A : Set} {x y : A}
   x  y
    -----
   y  x
sym refl = refl

这个证明是怎么运作的呢?sym 参数的类型是 x ≡ y,但是等式的左手边被 refl 模式实例化了, 这要求 xy 相等。因此,等式的右手边需要一个类型为 x ≡ x 的项,用 refl 即可。

交互式地证明 sym 很有教育意义。首先,我们在左手边使用一个变量来表示参数,在右手边使用一个洞:

sym : ∀ {A : Set} {x y : A}
  → x ≡ y
    -----
  → y ≡ x
sym e = {! !}

如果我们进入这个洞,使用 C-c C-,,Agda 会告诉我们:

Goal: .y ≡ .x
————————————————————————————————————————————————————————————
e  : .x ≡ .y
.y : .A
.x : .A
.A : Set

在这个洞里,我们使用 C-c C-c e,Agda 会将 e 逐一展开为所有可能的构造子。 此处只有一个构造子:

sym : ∀ {A : Set} {x y : A}
  → x ≡ y
    -----
  → y ≡ x
sym refl = {! !}

如果我们再次进入这个洞,重新使用 C-c C-,,然后 Agda 现在会告诉我们:

 Goal: .x ≡ .x
 ————————————————————————————————————————————————————————————
 .x : .A
 .A : Set

这是一个重要的步骤—— Agda 发现了 xy 必须相等,才能与模式 refl 相匹配。

最后,我们回到洞里,使用 C-c C-r,Agda 将会把洞变成一个可以满足给定类型的构造子实例。

sym : ∀ {A : Set} {x y : A}
  → x ≡ y
    -----
  → y ≡ x
sym refl = refl

我们至此完成了与之前给出证明相同的证明。

传递性亦是很直接:

trans :  {A : Set} {x y z : A}
   x  y
   y  z
    -----
   x  z
trans refl refl  =  refl

同样,交互式地证明这个特性是一个很好的练习,尤其是观察 Agda 的已知内容根据参数的实例而变化的过程。

合同性和替换性

相等性满足合同性(Congruence)。如果两个项相等,那么对它们使用相同的函数, 其结果仍然相等:

cong :  {A B : Set} (f : A  B) {x y : A}
   x  y
    ---------
   f x  f y
cong f refl  =  refl

两个参数的函数也满足合同性:

cong₂ :  {A B C : Set} (f : A  B  C) {u x : A} {v y : B}
   u  x
   v  y
    -------------
   f u v  f x y
cong₂ f refl refl  =  refl

在函数上的等价性也满足合同性。如果两个函数是相等的,那么它们作用在同一项上的结果是相等的:

cong-app :  {A B : Set} {f g : A  B}
   f  g
    ---------------------
    (x : A)  f x  g x
cong-app refl x = refl

相等性也满足替换性(Substitution)。 如果两个值相等,其中一个满足某谓词,那么另一个也满足此谓词。

subst :  {A : Set} {x y : A} (P : A  Set)
   x  y
    ---------
   P x  P y
subst P refl px = px

谓词是对于某类型为 A 的值之上的命题;因为我们将命题视作类型, 谓词则是对于 A 参数化的一个类型。 例如,参考我们之前在 Relations 章节中的例子, evenodd 是自然数 之上的谓词。 (我们将在 Decidable 章节中比较谓词的两种表示方式:以归纳数据类型 A → Set 或者以布尔值函数 A → Bool。)

等式链

我们在此演示如何使用等式链来论证,正如本书中使用证明形式。我们将声明放在一个叫做 ≡-Reasoning 的模块里,与 Agda 标准库中的格式相对应。

module ≡-Reasoning {A : Set} where

  infix  1 begin_
  infixr 2 _≡⟨⟩_ step-≡
  infix  3 _∎

  begin_ :  {x y : A}
     x  y
      -----
     x  y
  begin x≡y  =  x≡y

  _≡⟨⟩_ :  (x : A) {y : A}
     x  y
      -----
     x  y
  x ≡⟨⟩ x≡y  =  x≡y

  step-≡ :  (x {y z} : A)  y  z  x  y  x  z
  step-≡ x y≡z x≡y  =  trans x≡y y≡z

  syntax step-≡ x y≡z x≡y  =  x ≡⟨  x≡y  y≡z

  _∎ :  (x : A)
      -----
     x  x
  x   =  refl

open ≡-Reasoning

这是我们第一次使用嵌套的模块。它包括了关键字 module 和后续的模块名、隐式或显式参数, 关键字 where,和模块中的内容(在缩进内)。模块里可以包括任何形式的声明,也可以包括其他模块。 嵌套的模块和本书每章节所定义的顶层模块相似,只是顶层模块不需要缩进。 打开(open)一个模块会把模块内的所有定义导入进当前的环境中。

这也是我们第一次使用语法声明,它指明了左侧的项可以写成右边的语法形式。 语法 x ≡⟨ x≡y ⟩ y≡z 继承了声明 step-≡ 时使用的中缀式声明 infixr 2, 这种特殊的语法只要导入了 step-≡ 标识符就能使用。

除了引入带特殊语法的 step-≡ 外,我们也可以直接声明 _≡⟨_⟩′_

_≡⟨_⟩′_ :  {A : Set} (x : A) {y z : A}
   x  y
   y  z
    -----
   x  z
x ≡⟨ x≡y ⟩′ y≡z  =  trans x≡y y≡z

间接使用它的原因是 step-≡ 反转了实参的顺序,这样能让 Agda 更高效地执行类型推导。 在 Lambda 一章中我们会遇到一些长等式链,因此效率是很重要的。

我们来看看如何用等式链证明传递性:

trans′ :  {A : Set} {x y z : A}
   x  y
   y  z
    -----
   x  z
trans′ {A} {x} {y} {z} x≡y y≡z =
  begin
    x
  ≡⟨ x≡y 
    y
  ≡⟨ y≡z 
    z
  

根据其定义,等式右边会被解析成如下:

begin (x ≡⟨ x≡y ⟩ (y ≡⟨ y≡z ⟩ (z ∎)))

这里 begin 的使用纯粹是装饰性的,因为它直接返回了其参数。其参数包括了 _≡⟨_⟩_ 作用于 xx≡yy ≡⟨ y≡z ⟩ (z ∎)。第一个参数是一个项 x, 而第二、第三个参数分别是等式 x ≡ yy ≡ z 的证明,它们在 _≡⟨_⟩_ 的定义中用 trans 连接起来,形成 x ≡ z 的证明。y ≡ z 的证明包括了 _≡⟨_⟩_ 作用于 yy≡zz ∎。第一个参数是一个项 y,而第二、第三个参数分别是等式 y ≡ zz ≡ z 的证明, 它们在 _≡⟨_⟩_ 的定义中用 trans 连接起来,形成 y ≡ z 的证明。最后,z ≡ z 的证明包括了 _∎ 作用于 z 之上,使用了 refl。经过化简,上述定义等同于:

trans x≡y (trans y≡z refl)

我们可以把任意等式链转化成一系列的 trans 的使用。这样的证明更加精简,但是更难以阅读。 的小窍门意味着等式链化简成为的一系列 trans 会以 trans e refl 结尾,尽管只需要 e 就足够了,这里的 e 是等式的证明。

Exercise trans and ≡-Reasoning (practice)

Sadly, we cannot use the definition of trans’ using ≡-Reasoning as the definition for trans. Can you see why? (Hint: look at the definition of _≡⟨_⟩_)

-- Your code goes here

等式链的另外一个例子

我们重新证明加法的交换律来作为等式链的第二个例子。我们首先重复自然数和加法的定义。 我们不能导入它们(正如本章节开头中所解释的那样),因为那样会产生一个冲突:

data  : Set where
  zero : 
  suc  :   

_+_ :     
zero    + n  =  n
(suc m) + n  =  suc (m + n)

为了节约空间,我们假设两条引理(而不是证明它们):

postulate
  +-identity :  (m : )  m + zero  m
  +-suc :  (m n : )  m + suc n  suc (m + n)

这是我们第一次使用假设(Postulate)。假设为一个标识符指定一个签名,但是不提供定义。 我们在这里假设之前证明过的东西,来节约空间。假设在使用时必须加以注意。如果假设的内容为假, 那么我们可以证明出任何东西。

我们接下来重复交换律的证明:

+-comm :  (m n : )  m + n  n + m
+-comm m zero =
  begin
    m + zero
  ≡⟨ +-identity m 
    m
  ≡⟨⟩
    zero + m
  
+-comm m (suc n) =
  begin
    m + suc n
  ≡⟨ +-suc m n 
    suc (m + n)
  ≡⟨ cong suc (+-comm m n) 
    suc (n + m)
  ≡⟨⟩
    suc n + m
  

论证的过程和之前的相似。我们在不需要解释的地方使用 _≡⟨⟩_,我们可以认为 _≡⟨⟩__≡⟨ refl ⟩_ 是等价的。

Agda 总是认为一个项与其化简的项是等价的。我们之所以可以写出

  suc (n + m)
≡⟨⟩
  suc n + m

是因为 Agda 认为它们是一样的。这也意味着我们可以交换两行的顺序,写出

  suc n + m
≡⟨⟩
  suc (n + m)

而 Agda 并不会反对。Agda 只会检查由 ≡⟨⟩ 隔开的项是否化简后相同。 而书写的顺序合不合理则是由我们自行决定。

练习 ≤-Reasoning (延伸)

章节 Relations 中的单调性证明亦可以用相似于 ≡-Reasoning 的,更易于理解的形式给出。 相似地来定义 ≤-Reasoning,并用其重新给出加法对于不等式是单调的证明。重写 +-monoˡ-≤+-monoʳ-≤+-mono-≤ 的定义。

-- 请将代码写在此处。

重写

考虑一个自然数的性质,比如说一个数是偶数。我们重复之前给出的定义:

data even :   Set
data odd  :   Set

data even where

  even-zero : even zero

  even-suc :  {n : }
     odd n
      ------------
     even (suc n)

data odd where
  odd-suc :  {n : }
     even n
      -----------
     odd (suc n)

在前面的部分中,我们证明了加法满足交换律。给定 even (m + n) 成立的证据,我们应当可以用它来做 even (n + m) 成立的证据。

Agda 对这种论证有特殊记法的支持——我们之前提到过的 rewrite 记法。来启用这种记法, 我们只用编译程序指令来告诉 Agda 什么类型对应相等性:

{-# BUILTIN EQUALITY _≡_ #-}

我们然后就可以如下证明求证的性质:

even-comm :  (m n : )
   even (m + n)
    ------------
   even (n + m)
even-comm m n ev  rewrite +-comm n m  =  ev

在这里,ev 包括了所有 even (m + n) 成立的证据,我们证明它亦可作为 even (n + m) 成立的证据。一般来说,关键字 rewrite 之后跟着一个等式的证明,这个等式被用于重写目标和任意作用域内变量的类型。

交互性地证明 even-comm 是很有帮助的。一开始,我们先给左边的参数赋予变量,给右手边放上一个洞:

even-comm : ∀ (m n : ℕ)
  → even (m + n)
    ------------
  → even (n + m)
even-comm m n ev = {! !}

如果我们进入洞里,输入 C-c C-,,Agda 会报告:

Goal: even (n + m)
————————————————————————————————————————————————————————————
ev : even (m + n)
n  : ℕ
m  : ℕ

现在我们加入重写:

even-comm : ∀ (m n : ℕ)
  → even (m + n)
    ------------
  → even (n + m)
even-comm m n ev rewrite +-comm n m = {! !}

如果我们再次进入洞里,并输入 C-c C-,,Agda 现在会报告:

Goal: even (m + n)
————————————————————————————————————————————————————————————
ev : even (m + n)
n  : ℕ
m  : ℕ

目标里的参数被交换了。现在 ev 显然满足目标条件,输入 C-c C-a 会用 ev 来填充这个洞。 命令 C-c C-a 可以进行自动搜索,检查作用域内的变量是否和目标有相同的类型。

多重重写

我们可以多次使用重写,以竖线隔开。举个例子,这里是加法交换律的第二个证明,使用重写而不是等式链:

+-comm′ :  (m n : )  m + n  n + m
+-comm′ zero    n  rewrite +-identity n             =  refl
+-comm′ (suc m) n  rewrite +-suc n m | +-comm′ m n  =  refl

这个证明更加的简短。之前的证明用 cong suc (+-comm m n) 作为使用归纳假设的说明, 而这里我们使用 +-comm m n 来重写就足够了,因为重写可以将合同性考虑在其中。尽管使用重写的证明更加的简短, 使用等式链的证明能容易理解,我们将尽可能的使用后者。

深入重写

rewrite 记法实际上是 with 抽象的一种应用:

even-comm′ :  (m n : )
   even (m + n)
    ------------
   even (n + m)
even-comm′ m n ev with   m + n  | +-comm m n
...                  | .(n + m) | refl       = ev

总的来着,我们可以在 with 后面跟上任何数量的表达式,用竖线分隔开,并且在每个等式中使用相同个数的模式。 我们经常将表达式和模式如上对齐。这个第一列表明了 m + nn + m 是相同的,第二列使用相应等式来证明的前述的断言。 注意在这里使用的点模式(Dot Pattern).(n + m)。点模式由一个点和一个表达式组成, 在其他信息迫使这个值和点模式中的值相等时使用。在这里,m + nn + m 由后续的 +-comm m nrefl 的匹配来识别。我们可能会认为第一种情况是多余的,因为第二种情况中才蕴涵了需要的信息。 但实际上 Agda 在这件事上很挑剔——省略第一条或者更换顺序会让 Agda 报告一个错误。(试一试你就知道!)

在这种情况中,我们也可以使用之前定义的替换函数来避免使用重写:

even-comm″ :  (m n : )
   even (m + n)
    ------------
   even (n + m)
even-comm″ m n  =  subst even (+-comm m n)

尽管如此,重写是 Agda 工具箱中很重要的一部分。我们会偶尔使用它,但是它有的时候是必要的。

莱布尼兹(Leibniz)相等性

我们使用的相等性断言的形式源于 Martin-Löf,于 1975 年发表。一个更早的形式源于莱布尼兹, 于 1686 年发表。莱布尼兹断言的相等性表示不可分辨的实体(Identity of Indiscernibles): 两个对象相等当且仅当它们满足完全相同的性质。这条原理有时被称作莱布尼兹定律(Leibniz’ Law), 与史波克定律紧密相关:「一个不造成区别的区别不是区别」。我们在这里定义莱布尼兹相等性, 并证明两个项满足莱布尼兹相等性当且仅当其满足 Martin-Löf 相等性。

莱布尼兹相等性一般如下来定义:x ≐ y 当每个对于 x 成立的性质 P 对于 y 也成立时成立。 可能这有些出乎意料,但是这个定义亦足够保证其相反的命题:每个对于 y 成立的性质 P 对于 x 也成立。

xy 为类型 A 的对象。我们定义 x ≐ y 成立,当每个对于类型 A 成立的谓词 P, 我们有 P x 蕴涵了 P y

_≐_ :  {A : Set} (x y : A)  Set₁
_≐_ {A} x y =  (P : A  Set)  P x  P y

我们不能在左手边使用 x ≐ y,取而代之我们使用 _≐_ {A} x y 来提供隐式参数 A,这样 A 可以出现在右手边。

这是我们第一次使用等级(Levels)。我们不能将 Set 赋予类型 Set,因为这会导致自相矛盾, 比如罗素悖论(Russell’s Paradox)或者 Girard 悖论。不同的是,我们有一个阶级的类型:其中 Set : Set₁Set₁ : Set₂,以此类推。实际上,Set 本身就是 Set₀ 的缩写。定义 _≐_ 的等式在右手边提到了 Set,因此签名中必须使用 Set₁。我们稍后将进一步介绍等级。

莱布尼兹相等性是自反和传递的。自反性由恒等函数的变种得来,传递性由函数组合的变种得来:

refl-≐ :  {A : Set} {x : A}
   x  x
refl-≐ P Px  =  Px

trans-≐ :  {A : Set} {x y z : A}
   x  y
   y  z
    -----
   x  z
trans-≐ x≐y y≐z P Px  =  y≐z P (x≐y P Px)

对称性就没有那么显然了。我们需要证明如果对于所有谓词 PP x 蕴涵 P y, 那么反方向的蕴涵也成立。

sym-≐ :  {A : Set} {x y : A}
   x  y
    -----
   y  x
sym-≐ {A} {x} {y} x≐y P  =  Qy
  where
    Q : A  Set
    Q z = P z  P x
    Qx : Q x
    Qx = refl-≐ P
    Qy : Q y
    Qy = x≐y Q Qx

给定 x ≐ y 和一个特定的 P,我们需要构造一个 P y 蕴涵 P x 的证明。 我们首先用一个谓词 Q 将相等性实例化,使得 Q zP z 蕴涵 P x 时成立。 Q x 这个性质是显然的,由自反性可以得出,由此通过 x ≐ y 就能推出 Q y 成立。而 Q y 正是我们需要的证明,即 P y 蕴涵 P x

我们现在来证明 Martin-Löf 相等性蕴涵了莱布尼兹相等性,以及其逆命题。在正方向上, 如果我们已知 x ≡ y,我们需要对于任意的 P,将 P x 的证明转换为 P y 的证明。 我们很容易就可以做到这一点,因为 xy 相等意味着任何 P x 的证明即是 P y 的证明。

≡-implies-≐ :  {A : Set} {x y : A}
   x  y
    -----
   x  y
≡-implies-≐ x≡y P  =  subst P x≡y

因为这个方向由替换性可以得来,如之前证明的那样。

在反方向上,我们已知对于任何 P,我们可以将 P x 的证明转换成 P y 的证明, 我们需要证明 x ≡ y

≐-implies-≡ :  {A : Set} {x y : A}
   x  y
    -----
   x  y
≐-implies-≡ {A} {x} {y} x≐y  =  Qy
  where
    Q : A  Set
    Q z = x  z
    Qx : Q x
    Qx = refl
    Qy : Q y
    Qy = x≐y Q Qx

此证明与莱布尼兹相等性的对称性证明相似。我们取谓词 Q,使得 Q zx ≡ z 成立时成立。 那么 Q x 是显然的,由 Martin Löf 相等性的自反性得来。从而 Q yx ≐ y 可得, 而 Q y 即是我们所需要的 x ≡ y 的证明。

(本部分的内容由此处改编得来: ≐≃≡: Leibniz Equality is Isomorphic to Martin-Löf Identity, Parametrically 作者:Andreas Abel、Jesper Cockx、Dominique Devries、Andreas Nuyts 与 Philip Wadler, 草稿,2017)

全体多态

正如我们之前看到的那样,不是每个类型都属于 Set,但是每个类型都属于类型阶级的某处, Set₀Set₁Set₂等等。其中 SetSet₀ 的缩写,此外 Set₀ : Set₁Set₁ : Set₂,以此类推。 当我们需要比较两个属于 Set 的类型的值时,我们之前给出的定义是足够的, 但如果我们需要比较对于任何等级 ,两个属于 Set ℓ 的类型的值该怎么办呢?

答案是全体多态(Universe Polymorphism),一个定义可以根据任何等级 来做出。 为了使用等级,我们首先导入下列内容:

open import Level using (Level; _⊔_) renaming (zero to lzero; suc to lsuc)

我们将构造子 zerosuc 重命名至 lzerolsuc,为了防止自然数和等级之间的混淆。

等级与自然数是同构的,有相似的构造子:

lzero : Level
lsuc  : Level → Level

Set₀Set₁Set₂ 等名称,是下列的简写:

Set lzero
Set (lsuc lzero)
Set (lsuc (lsuc lzero))

以此类推。我们还有一个运算符:

_⊔_ : Level → Level → Level

给定两个等级,返回两者中较大的那个。

下面是相等性的定义,推广到任意等级:

data _≡′_ { : Level} {A : Set } (x : A) : A  Set  where
  refl′ : x ≡′ x

相似的,下面是对称性的推广定义:

sym′ :  { : Level} {A : Set } {x y : A}
   x ≡′ y
    ------
   y ≡′ x
sym′ refl′ = refl′

为了简洁,我们在本书中给出的定义将避免使用全体多态,但是大多数标准库中的定义, 包括相等性的定义,都推广到了任意等级,如上所示。

下面是莱布尼兹相等性的推广定义:

_≐′_ :  { : Level} {A : Set } (x y : A)  Set (lsuc )
_≐′_ {} {A} x y =  (P : A  Set )  P x  P y

之前,签名中使用了 Set₁ 来作为一个值包括了 Set 的类型;而此处,我们使用 Set (lsuc ℓ) 来作为一个值包括了 Set ℓ 的类型。

标准库中的大部分函数都泛化到了任意层级。例如,以下是复合的定义。

_∘_ :  {ℓ₁ ℓ₂ ℓ₃ : Level} {A : Set ℓ₁} {B : Set ℓ₂} {C : Set ℓ₃}
   (B  C)  (A  B)  A  C
(g  f) x  =  g (f x)

更多关于层级的信息可以从Agda 文档中查询。

标准库

标准库中可以找到与本章节中相似的定义。Agda 标准库将 _≡⟨_⟩_ 定义为 step-≡它反转了参数的顺序。标准库还定义了一个语法宏,它可以在你导入 step-≡ 时被自动导入,它能够恢复原始的参数顺序:

-- import Relation.Binary.PropositionalEquality as Eq
-- open Eq using (_≡_; refl; trans; sym; cong; cong-app; subst)
-- open Eq.≡-Reasoning using (begin_; _≡⟨⟩_; step-≡; _∎)

这里的导入以注释的形式给出,以防止冲突,如引言中解释的那样。

Unicode

本章节使用下列 Unicode:

≡  U+2261  等同于 (\==, \equiv)
⟨  U+27E8  数学左尖括号 (\<)
⟩  U+27E9  数学右尖括号 (\>)
∎  U+220E  证毕 (\qed)
≐  U+2250  趋近于极限 (\.=)
ℓ  U+2113  手写小写 L (\ell)
⊔  U+2294  正方形向上开口 (\lub)
₀  U+2080  下标 0 (\_0)
₁  U+2081  下标 1 (\_1)
₂  U+2082  下标 2 (\_2)

Isomorphism: 同构与嵌入

module plfa.part1.Isomorphism where

本部分介绍同构(Isomorphism)与嵌入(Embedding)。 同构可以断言两个类型是相等的,嵌入可以断言一个类型比另一个类型小。 我们会在下一章中使用同构来展示类型上的运算,例如积或者和,满足类似于交换律、结合律和分配律的性质。

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl; cong; cong-app)
open Eq.≡-Reasoning
open import Data.Nat using (; zero; suc; _+_)
open import Data.Nat.Properties using (+-comm)

Lambda 表达式

本章节开头将补充一些有用的基础知识:lambda 表达式,函数组合,以及外延性。

Lambda 表达式提供了一种简洁的定义函数的方法,且不需要提供函数名。一个如同这样的项:

λ{ P₁ → N₁; ⋯ ; Pₙ → Nₙ }

等同于定义一个函数 f,使用下列等式:

f P₁ = N₁
⋯
f Pₙ = Nₙ

其中 Pₙ 是模式(即等式的左手边),Nₙ 是表达式(即等式的右手边)。

如果只有一个等式,且模式是一个变量,我们亦可使用下面的语法:

λ x → N

或者

λ (x : A) → N

两个都与 λ{x → N} 等价。后者可以指定函数的作用域。

往往使用匿名的 lambda 表达式比使用带名字的函数要方便:它避免了冗长的类型声明; 其定义出现在其使用的地方,所以在书写时不需要记得提前声明,在阅读时不需要上下搜索函数定义。

函数组合 (Function Composition)

接下来,我们将使用函数组合:

_∘_ :  {A B C : Set}  (B  C)  (A  B)  (A  C)
(g  f) x  = g (f x)

g ∘ f 是一个函数,先使用函数 f,再使用函数 g。 一个等价的定义,使用 lambda 表达式,如下:

_∘′_ :  {A B C : Set}  (B  C)  (A  B)  (A  C)
g ∘′ f  =  λ x  g (f x)

外延性(Extensionality)

外延性断言了区分函数的唯一方法是应用它们。如果两个函数作用在相同的参数上永远返回相同的结果, 那么两个函数相同。这是 cong-app 的逆命题,在之前有所介绍。

Agda 并不预设外延性,但我们可以假设其成立:

postulate
  extensionality :  {A B : Set} {f g : A  B}
     (∀ (x : A)  f x  g x)
      -----------------------
     f  g

假设外延性不会造成困顿,因为我们知道它与 Agda 使用的理论是连贯一致的。

举个例子,我们考虑两个库都定义了加法,一个按照我们在 Naturals 章节中那样定义,另一个如下,反过来定义:

_+′_ :     
m +′ zero  = m
m +′ suc n = suc (m +′ n)

通过使用交换律,我们可以简单地证明两个运算符在给定相同参数的情况下, 会返回相同的值:

same-app :  (m n : )  m +′ n  m + n
same-app m n rewrite +-comm m n = helper m n
  where
  helper :  (m n : )  m +′ n  n + m
  helper m zero    = refl
  helper m (suc n) = cong suc (helper m n)

然而,有时断言两个运算符是无法区分的会更加方便。我们可以使用两次外延性:

same : _+′_  _+_
same = extensionality  m  extensionality  n  same-app m n))

我们偶尔需要在之后的情况中假设外延性。

More generally, we may wish to postulate extensionality for dependent functions.
postulate
  ∀-extensionality :  {A : Set} {B : A  Set} {f g : ∀(x : A)  B x}
     (∀ (x : A)  f x  g x)
      -----------------------
     f  g

Here the type of f and g has changed from A → B to ∀ (x : A) → B x, generalising ordinary functions to dependent functions.

同构(Isomorphism)

如果两个集合有一一对应的关系,那么它们是同构的。 下面是同构的正式定义:

infix 0 _≃_
record _≃_ (A B : Set) : Set where
  field
    to   : A  B
    from : B  A
    from∘to :  (x : A)  from (to x)  x
    to∘from :  (y : B)  to (from y)  y
open _≃_

我们来一一展开这个定义。一个集合 AB 之间的同构有四个要素: 1. 从 AB 的函数 to 2. 从 B 回到 A 的函数 from 3. fromto左逆(left-inverse)的证明 from∘to 4. fromto右逆(right-inverse)的证明 to∘from

具体来说,第三条断言了 from ∘ to 是恒等函数,第四条断言了 to ∘ from 是恒等函数, 它们的名称由此得来。声明 open _≃_ 使得 tofromfrom∘toto∘from 在当前作用域内可用,否则我们需要使用类似 _≃_.to 的写法。

这是我们第一次使用记录(Record)。记录声明的行为类似于一个单构造子的数据声明 (二者稍微有些不同,我们会在 Connectives 一章中讨论):

data _≃′_ (A B : Set): Set where
  mk-≃′ :  (to : A  B) 
           (from : B  A) 
           (from∘to : (∀ (x : A)  from (to x)  x)) 
           (to∘from : (∀ (y : B)  to (from y)  y)) 
          A ≃′ B

to′ :  {A B : Set}  (A ≃′ B)  (A  B)
to′ (mk-≃′ f g g∘f f∘g) = f

from′ :  {A B : Set}  (A ≃′ B)  (B  A)
from′ (mk-≃′ f g g∘f f∘g) = g

from∘to′ :  {A B : Set}  (A≃B : A ≃′ B)  (∀ (x : A)  from′ A≃B (to′ A≃B x)  x)
from∘to′ (mk-≃′ f g g∘f f∘g) = g∘f

to∘from′ :  {A B : Set}  (A≃B : A ≃′ B)  (∀ (y : B)  to′ A≃B (from′ A≃B y)  y)
to∘from′ (mk-≃′ f g g∘f f∘g) = f∘g

我们用下面的语法来构造一个记录类型的值:

record
  { to    = f
  ; from  = g
  ; from∘to = g∘f
  ; to∘from = f∘g
  }

这与使用相应的归纳类型的构造子对应:

mk-≃′ f g g∘f f∘g

其中 fgg∘ff∘g 是相应类型的值。

同构是一个等价关系

同构是一个等价关系。这意味着它自反、对称、传递。要证明同构是自反的,我们用恒等函数 作为 tofrom

≃-refl :  {A : Set}
    -----
   A  A
≃-refl =
  record
    { to      = λ{x  x}
    ; from    = λ{y  y}
    ; from∘to = λ{x  refl}
    ; to∘from = λ{y  refl}
    }

如上,tofrom 都是恒等函数,from∘toto∘from 都是丢弃参数、返回 refl 的函数。在这样的情况下,refl 足够可以证明左逆,因为 from (to x) 化简为 x。右逆的证明同理。

要证明同构是对称的,我们把 tofromfrom∘toto∘from 互换:

≃-sym :  {A B : Set}
   A  B
    -----
   B  A
≃-sym A≃B =
  record
    { to      = from A≃B
    ; from    = to   A≃B
    ; from∘to = to∘from A≃B
    ; to∘from = from∘to A≃B
    }

要证明同构是传递的,我们将 tofrom 函数进行组合,并使用相等性论证来结合左逆和右逆:

≃-trans :  {A B C : Set}
   A  B
   B  C
    -----
   A  C
≃-trans A≃B B≃C =
  record
    { to       = to   B≃C  to   A≃B
    ; from     = from A≃B  from B≃C
    ; from∘to  = λ{x 
        begin
          (from A≃B  from B≃C) ((to B≃C  to A≃B) x)
        ≡⟨⟩
          from A≃B (from B≃C (to B≃C (to A≃B x)))
        ≡⟨ cong (from A≃B) (from∘to B≃C (to A≃B x)) 
          from A≃B (to A≃B x)
        ≡⟨ from∘to A≃B x 
          x
        }
    ; to∘from = λ{y 
        begin
          (to B≃C  to A≃B) ((from A≃B  from B≃C) y)
        ≡⟨⟩
          to B≃C (to A≃B (from A≃B (from B≃C y)))
        ≡⟨ cong (to B≃C) (to∘from A≃B (from B≃C y)) 
          to B≃C (from B≃C y)
        ≡⟨ to∘from B≃C y 
          y
        }
     }

同构的相等性论证

我们可以直接的构造一种同构的相等性论证方法。我们对之前的相等性论证定义进行修改。 我们省略 _≡⟨⟩_ 的定义,因为简单的同构比简单的相等性出现的少很多:

module ≃-Reasoning where

  infix  1 ≃-begin_
  infixr 2 _≃⟨_⟩_
  infix  3 _≃-∎

  ≃-begin_ :  {A B : Set}
     A  B
      -----
     A  B
  ≃-begin A≃B = A≃B

  _≃⟨_⟩_ :  (A : Set) {B C : Set}
     A  B
     B  C
      -----
     A  C
  A ≃⟨ A≃B  B≃C = ≃-trans A≃B B≃C

  _≃-∎ :  (A : Set)
      -----
     A  A
  A ≃-∎ = ≃-refl

open ≃-Reasoning

嵌入(Embedding)

我们同时也需要嵌入的概念,它是同构的弱化概念。同构要求证明两个类型之间的一一对应, 而嵌入只需要第一种类型涵盖在第二种类型内,所以两个类型之间有一对多的对应关系。

嵌入的正式定义如下:

infix 0 _≲_
record _≲_ (A B : Set) : Set where
  field
    to      : A  B
    from    : B  A
    from∘to :  (x : A)  from (to x)  x
open _≲_

除了它缺少了 to∘from 字段以外,嵌入的定义和同构是一样的。因此,我们可以得知 fromto 的左逆,但是 from 不是 to 的右逆。

嵌入是自反和传递的,但不是对称的。证明与同构类似,不过去除了不需要的部分:

≲-refl :  {A : Set}  A  A
≲-refl =
  record
    { to      = λ{x  x}
    ; from    = λ{y  y}
    ; from∘to = λ{x  refl}
    }

≲-trans :  {A B C : Set}  A  B  B  C  A  C
≲-trans A≲B B≲C =
  record
    { to      = λ{x  to   B≲C (to   A≲B x)}
    ; from    = λ{y  from A≲B (from B≲C y)}
    ; from∘to = λ{x 
        begin
          from A≲B (from B≲C (to B≲C (to A≲B x)))
        ≡⟨ cong (from A≲B) (from∘to B≲C (to A≲B x)) 
          from A≲B (to A≲B x)
        ≡⟨ from∘to A≲B x 
          x
        }
     }

显而易见的是,如果两个类型相互嵌入,且其嵌入函数相互对应,那么它们是同构的。 这个一种反对称性的弱化形式:

≲-antisym :  {A B : Set}
   (A≲B : A  B)
   (B≲A : B  A)
   (to A≲B  from B≲A)
   (from A≲B  to B≲A)
    -------------------
   A  B
≲-antisym A≲B B≲A to≡from from≡to =
  record
    { to      = to A≲B
    ; from    = from A≲B
    ; from∘to = from∘to A≲B
    ; to∘from = λ{y 
        begin
          to A≲B (from A≲B y)
        ≡⟨ cong (to A≲B) (cong-app from≡to y) 
          to A≲B (to B≲A y)
        ≡⟨ cong-app to≡from (to B≲A y) 
          from B≲A (to B≲A y)
        ≡⟨ from∘to B≲A y 
          y
        }
    }

前三部分可以直接从嵌入中得来,最后一部分我们可以把 B ≲ A 中的左逆和 两个嵌入中的 tofrom 部分的相等性来获得同构中的右逆。

嵌入的相等性论证

和同构类似,我们亦支持嵌入的相等性论证:

module ≲-Reasoning where

  infix  1 ≲-begin_
  infixr 2 _≲⟨_⟩_
  infix  3 _≲-∎

  ≲-begin_ :  {A B : Set}
     A  B
      -----
     A  B
  ≲-begin A≲B = A≲B

  _≲⟨_⟩_ :  (A : Set) {B C : Set}
     A  B
     B  C
      -----
     A  C
  A ≲⟨ A≲B  B≲C = ≲-trans A≲B B≲C

  _≲-∎ :  (A : Set)
      -----
     A  A
  A ≲-∎ = ≲-refl

open ≲-Reasoning
练习 ≃-implies-≲(实践)

证明每个同构蕴涵了一个嵌入。

postulate
  ≃-implies-≲ :  {A B : Set}
     A  B
      -----
     A  B
-- 请将代码写在此处。
练习 _⇔_(实践)

按下列形式定义命题的等价性(又名「当且仅当」):

record _⇔_ (A B : Set) : Set where
  field
    to   : A  B
    from : B  A

证明等价性是自反、对称和传递的。

-- 请将代码写在此处。
练习 Bin-embedding (延伸)

回忆练习 BinBin-laws, 我们定义了比特串数据类型 Bin 来表示自然数,并要求你来定义下列函数:

to : ℕ → Bin
from : Bin → ℕ

它们满足如下性质:

from (to n) ≡ n

使用上述条件,证明存在一个从 Bin 的嵌入。

-- 请将代码写在此处。

为什么 tofrom 不能构造一个同构?

标准库

标准库中可以找到与本章节中相似的定义:

import Function using (_∘_)
import Function.Inverse using (_↔_)
import Function.LeftInverse using (_↞_)

标准库中的 _↔︎__↞_ 分别对应了我们定义的 _≃__≲_, 但是标准库中的定义使用起来不如我们的定义方便,因为标准库中的定义依赖于一个嵌套的记录结构, 并可以由任何相等性的记法来参数化。

Unicode

本章节使用了如下 Unicode:

∘  U+2218  环运算符 (\o, \circ, \comp)
λ  U+03BB  小写希腊字母 LAMBDA (\lambda, \Gl)
≃  U+2243  渐进相等 (\~-)
≲  U+2272  小于或等价于 (\<~)
⇔  U+21D4  左右双箭头 (\<=>)

Connectives: 合取、析取与蕴涵

module plfa.part1.Connectives where

本章节介绍基础的逻辑运算符。我们使用逻辑运算符与数据类型之间的对应关系, 即命题即类型(Propositions as Types)原理。

  • 合取(Conjunction)即是积(Product)
  • 析取(Disjunction)即是和(Sum)
  • 真(True)即是单元类型(Unit Type)
  • 假(False)即是空类型(Empty Type)
  • 蕴涵(Implication)即是函数空间(Function Space)

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl)
open Eq.≡-Reasoning
open import Data.Nat using ()
open import Function using (_∘_)
open import plfa.part1.Isomorphism using (_≃_; _≲_; extensionality; _⇔_)
open plfa.part1.Isomorphism.≃-Reasoning

合取即是积

给定两个命题 AB,其合取 A × B 成立当 A 成立和 B 成立。 我们用一个合适的数据类型将这样的概念形式化:

data _×_ (A B : Set) : Set where

  ⟨_,_⟩ :
      A
     B
      -----
     A × B

A × B 成立的证明由 ⟨ M , N ⟩ 的形式表现,其中 MA 成立的证明, NB 成立的证明。

给定 A × B 成立的证明,我们可以得出 A 成立和 B 成立。

proj₁ :  {A B : Set}
   A × B
    -----
   A
proj₁  x , y  = x

proj₂ :  {A B : Set}
   A × B
    -----
   B
proj₂  x , y  = y

如果 LA × B 成立的证据, 那么 proj₁ LA 成立的证据, proj₂ LB 成立的证据。

⟨_,_⟩ 在等式右手边的项中出现的时候,我们将其称作构造子(Constructor), 当它出现在等式左边时,我们将其称作解构子(Destructor)。我们亦可将 proj₁proj₂ 称作解构子,因为它们起到相似的效果。

其他的术语将 ⟨_,_⟩ 称作引入(Introduce)合取,将 proj₁proj₂ 称作消去(Eliminate)合取。 前者亦记作 ×-I,后者 ×-E₁×-E₂。如果我们从上到下来阅读这些规则,引入和消去 正如其名字所说的那样:第一条引入一个运算符,所以运算符出现在结论中,而不是假设中; 第二条消去一个带有运算符的式子,而运算符出现在假设中,而不是结论中。引入规则描述了 运算符在什么情况下成立——即怎么样定义一个运算符。消去规则描述了运算符成立时,可以得出 什么样的结论——即怎么样使用一个运算符。1

在这样的情况下,先使用解构子,再使用构造子将结果重组,得到还是原来的积。

η-× :  {A B : Set} (w : A × B)   proj₁ w , proj₂ w   w
η-×  x , y  = refl

左手边的模式匹配是必要的。用 ⟨ x , y ⟩ 来替换 w 让等式的两边可以化简成相同的项。

我们设置合取的优先级,使它与除了析取之外结合的都不紧密:

infixr 2 _×_

因此,m ≤ n × n ≤ p 解析为 (m ≤ n) × (n ≤ p)

Alternatively, we can declare conjunction as a record type:

record _×′_ (A B : Set) : Set where
  constructor ⟨_,_⟩′
  field
    proj₁′ : A
    proj₂′ : B
open _×′_

The record construction record { proj₁′ = M ; proj₂′ = N } corresponds to the term ⟨ M , N ⟩ where M is a term of type A and N is a term of type B. The constructor declaration allows us to write ⟨ M , N ⟩′ in place of the record construction.

The data type _×_ and the record type _×′_ behave similarly. One difference is that for data types we have to prove η-equality, but for record types, η-equality holds by definition. While proving η-×′, we do not have to pattern match on w to know that η-equality holds:

η-×′ :  {A B : Set} (w : A ×′ B)   proj₁′ w , proj₂′ w ⟩′  w
η-×′ w = refl

It can be very convenient to have η-equality definitionally, and so the standard library defines _×_ as a record type. We use the definition from the standard library in later chapters.

给定两个类型 AB,我们将 A × B 称为 AB。 在集合论中它也被称作笛卡尔积(Cartesian Product),在计算机科学中它对应记录类型。 如果类型 Am 个不同的成员,类型 Bn 个不同的成员, 那么类型 A × Bm * n 个不同的成员。这也是它被称为积的原因之一。 例如,考虑有两个成员的 Bool 类型,和有三个成员的 Tri 类型:

data Bool : Set where
  true  : Bool
  false : Bool

data Tri : Set where
  aa : Tri
  bb : Tri
  cc : Tri

那么,Bool × Tri 类型有如下的六个成员:

⟨ true  , aa ⟩    ⟨ true  , bb ⟩    ⟨ true ,  cc ⟩
⟨ false , aa ⟩    ⟨ false , bb ⟩    ⟨ false , cc ⟩

下面的函数枚举了所有类型为 Bool × Tri 的参数:

×-count : Bool × Tri  
×-count  true  , aa   =  1
×-count  true  , bb   =  2
×-count  true  , cc   =  3
×-count  false , aa   =  4
×-count  false , bb   =  5
×-count  false , cc   =  6

类型上的积与数的积有相似的性质——它们满足交换律和结合律。 更确切地说,积在在同构意义下满足交换律和结合率。

对于交换律,to 函数将有序对交换,将 ⟨ x , y ⟩ 变为 ⟨ y , x ⟩from 函数亦是如此(忽略命名)。 在 from∘toto∘from 中正确地实例化要匹配的模式是很重要的。 使用 λ w → refl 作为 from∘to 的定义是不可行的,to∘from 同理。

×-comm :  {A B : Set}  A × B  B × A
×-comm =
  record
    { to       =  λ{  x , y    y , x  }
    ; from     =  λ{  y , x    x , y  }
    ; from∘to  =  λ{  x , y   refl }
    ; to∘from  =  λ{  y , x   refl }
    }

满足交换律在同构意义下满足交换律是不一样的。比较下列两个命题:

m * n ≡ n * m
A × B ≃ B × A

在第一个情况下,我们可能有 m2n3,那么 m * nn * m 都是 6。 在第二个情况下,我们可能有 ABoolBTri,但是 Bool × TriTri × Bool 不是一样的。但是存在一个两者之间的同构。例如:⟨ true , aa ⟩ 是前者的成员, 其对应后者的成员 ⟨ aa , true ⟩

对于结合律来说,to 函数将两个有序对进行重组:将 ⟨ ⟨ x , y ⟩ , z ⟩ 转换为 ⟨ x , ⟨ y , z ⟩ ⟩from 函数则为其逆。同样,左逆和右逆的证明需要在一个合适的模式来匹配,从而可以直接化简:

×-assoc :  {A B C : Set}  (A × B) × C  A × (B × C)
×-assoc =
  record
    { to      = λ{   x , y  , z    x ,  y , z   }
    ; from    = λ{  x ,  y , z      x , y  , z  }
    ; from∘to = λ{   x , y  , z   refl }
    ; to∘from = λ{  x ,  y , z    refl }
    }

满足结合律在同构意义下满足结合律是不一样的。比较下列两个命题:

(m * n) * p ≡ m * (n * p)
(A × B) × C ≃ A × (B × C)

举个例子,(ℕ × Bool) × Triℕ × (Bool × Tri) 不同,但是两个类型之间 存在同构。例如 ⟨ ⟨ 1 , true ⟩ , aa ⟩,一个前者的成员,与 ⟨ 1 , ⟨ true , aa ⟩ ⟩, 一个后者的成员,相对应。

练习 ⇔≃×(实践)

证明之前定义的 A ⇔ B(A → B) × (B → A) 同构。

-- 请将代码写在此处。

真即是单元类型

恒真 恒成立。我们将这个概念用合适的数据类型来形式化:

data  : Set where

  tt :
    --
    

成立的证明由 tt 的形式构成。

恒真有引入规则,但没有消去规则。给定一个 成立的证明,我们不能得出任何有趣的结论。 因为恒真恒成立,知道恒真成立不会给我们带来新的知识。

η-× 的 零元形式是 η-⊤,其断言了任何 类型的值一定等于 tt

η-⊤ :  (w : )  tt  w
η-⊤ tt = refl

左手边的模式匹配是必要的。将 w 替换为 tt 让等式两边可以化简为相同的值。

Alternatively, we can declare truth as an empty record:
record ⊤′ : Set where
  constructor tt′

The record construction record {} corresponds to the term tt. The constructor declaration allows us to write tt′.

As with the product, the data type and the record type ⊤′ behave similarly, but η-equality holds by definition for the record type. While proving η-⊤′, we do not have to pattern match on w—Agda knows it is equal to tt′:
η-⊤′ :  (w : ⊤′)  tt′  w
η-⊤′ w = refl
Agda knows that any value of type ⊤′ must be tt′, so any time we need a value of type ⊤′, we can tell Agda to figure it out:
truth′ : ⊤′
truth′ = _

我们将 称为单元(Unit Type)类型。实际上, 类型只有一个成员 tt。 例如,下面的函数枚举了所有 类型的参数:

⊤-count :   
⊤-count tt = 1

对于数来说,1 是乘法的幺元。对应地,单元是积的幺元(在同构意义下)。对于左幺元来说, to 函数将 ⟨ tt , x ⟩ 转换成 xfrom 函数则是其反函数。左逆的证明需要 匹配一个合适的模式来化简:

⊤-identityˡ :  {A : Set}   × A  A
⊤-identityˡ =
  record
    { to      = λ{  tt , x   x }
    ; from    = λ{ x   tt , x  }
    ; from∘to = λ{  tt , x   refl }
    ; to∘from = λ{ x  refl }
    }

幺元在同构意义下的幺元是不一样的。比较下列两个命题:

1 * m ≡ m
⊤ × A ≃ A

在第一种情况下,我们可能有 m2,那么 1 * mm 都为 2。 在第二种情况下,我们可能有 ABool,但是 ⊤ × BoolBool 是不同的。 例如:⟨ tt , true ⟩ 是前者的成员,其对应后者的成员 true

右幺元可以由积的交换律得来:

⊤-identityʳ :  {A : Set}  (A × )  A
⊤-identityʳ {A} =
  ≃-begin
    (A × )
  ≃⟨ ×-comm 
    ( × A)
  ≃⟨ ⊤-identityˡ 
    A
  ≃-∎

我们在此使用了同构链,与等式链相似。

析取即是和

给定两个命题 AB,析取 A ⊎ BA 成立或者 B 成立时成立。 我们将这个概念用合适的归纳类型来形式化:

data _⊎_ (A B : Set) : Set where

  inj₁ :
      A
      -----
     A  B

  inj₂ :
      B
      -----
     A  B

A ⊎ B 成立的证明有两个形式: inj₁ M,其中 MA 成立的证明,或者 inj₂ N,其中 NB 成立的证明。

给定 A → CB → C 成立的证明,那么给定一个 A ⊎ B 的证明,我们可以得出 C 成立:

case-⊎ :  {A B C : Set}
   (A  C)
   (B  C)
   A  B
    -----------
   C
case-⊎ f g (inj₁ x) = f x
case-⊎ f g (inj₂ y) = g y

inj₁inj₂ 进行模式匹配,是我们使用析取成立的证明的常见方法。

inj₁inj₂ 在等式右手边出现的时候,我们将其称作构造子, 当它出现在等式左边时,我们将其称作解构子。我们亦可将 case-⊎ 称作解构子,因为它们起到相似的效果。其他术语将 inj₁inj₂ 称为引入析取, 将 case-⊎ 称为消去析取。前者亦被称为 ⊎-I₁⊎-I₂,后者 ⊎-E

对每个构造子使用解构子得到的是原来的值:

η-⊎ :  {A B : Set} (w : A  B)  case-⊎ inj₁ inj₂ w  w
η-⊎ (inj₁ x) = refl
η-⊎ (inj₂ y) = refl

更普遍地来说,我们亦可对于析取使用一个任意的函数:

uniq-⊎ :  {A B C : Set} (h : A  B  C) (w : A  B) 
  case-⊎ (h  inj₁) (h  inj₂) w  h w
uniq-⊎ h (inj₁ x) = refl
uniq-⊎ h (inj₂ y) = refl

左手边的模式匹配是必要的。用 inj₁ x 来替换 w 让等式的两边可以化简成相同的项, inj₂ y 同理。

我们设置析取的优先级,使它与任何已经定义的运算符都结合的不紧密:

infixr 1 _⊎_

因此 A × C ⊎ B × C 解析为 (A × C) ⊎ (B × C)

给定两个类型 AB,我们将 A ⊎ B 称为 AB。 在集合论中它也被称作不交并(Disjoint Union),在计算机科学中它对应变体记录类型。 如果类型 Am 个不同的成员,类型 Bn 个不同的成员, 那么类型 A ⊎ Bm + n 个不同的成员。这也是它被称为和的原因之一。 例如,考虑有两个成员的 Bool 类型,和有三个成员的 Tri 类型,如之前的定义。 那么,Bool ⊎ Tri 类型有如下的五个成员:

inj₁ true     inj₂ aa
inj₁ false    inj₂ bb
              inj₂ cc

下面的函数枚举了所有类型为 Bool ⊎ Tri 的参数:

⊎-count : Bool  Tri  
⊎-count (inj₁ true)   =  1
⊎-count (inj₁ false)  =  2
⊎-count (inj₂ aa)     =  3
⊎-count (inj₂ bb)     =  4
⊎-count (inj₂ cc)     =  5

类型上的和与数的和有相似的性质——它们满足交换律和结合律。 更确切地说,和在在同构意义下是交换和结合的。

练习 ⊎-comm (推荐)

证明和类型在同构意义下满足交换律。

-- 请将代码写在此处。
练习 ⊎-assoc(实践)

证明和类型在同构意义下满足结合律。

-- 请将代码写在此处。

假即是空类型

恒假 从不成立。我们将这个概念用合适的归纳类型来形式化:

data  : Set where
  -- 没有语句!

没有 成立的证明。

相对偶, 没有引入规则,但是有消去规则。因为恒假从不成立, 如果它一旦成立,我们就进入了矛盾之中。给定 成立的证明,我们可以得出任何结论! 这是逻辑学的基本原理,又由中世纪的拉丁文词组 ex falso 为名。小孩子也由诸如 「如果猪有翅膀,那我就是示巴女王」的词组中知晓。我们如下将它形式化:

⊥-elim :  {A : Set}
   
    --
   A
⊥-elim ()

这是我们第一次使用荒谬模式(Absurd Pattern) ()。在这里,因为 是一个没有成员的类型,我们用 () 模式来指明这里不可能匹配任何这个类型的值。

case-⊎ 的零元形式是 ⊥-elim。类比的来说,它应该叫做 case-⊥, 但是我们在此使用标准库中使用的名字。

uniq-⊎ 的零元形式是 uniq-⊥,其断言了 ⊥-elim 和任何取 的函数是等价的。

uniq-⊥ :  {C : Set} (h :   C) (w : )  ⊥-elim w  h w
uniq-⊥ h ()

使用荒谬模式断言了 w 没有任何可能的值,因此等式显然成立。

我们将 成为空(Empty)类型。实际上, 类型没有成员。 例如,下面的函数枚举了所有 类型的参数:

⊥-count :   
⊥-count ()

同样,荒谬模式告诉我们没有值可以来匹配类型

对于数来说,0 是加法的幺元。对应地,空是和的幺元(在同构意义下)。

练习 ⊥-identityˡ (推荐)

证明空在同构意义下是和的左幺元。

-- 请将代码写在此处。
Exercise ⊥-identityʳ (practice)
练习 ⊥-identityʳ(实践)

证明空在同构意义下是和的右幺元。

-- 请将代码写在此处。

蕴涵即是函数

给定两个命题 AB,其蕴涵 A → B 在任何 A 成立的时候,B 也成立时成立。 我们用函数类型来形式化蕴涵,如本书中通篇出现的那样。

A → B 成立的证据由下面的形式组成:

λ (x : A) → N

其中 N 是一个类型为 B 的项,其包括了一个类型为 A 的自由变量 x。 给定一个 A → B 成立的证明 L,和一个 A 成立的证明 M,那么 L MB 成立的证明。 也就是说,A → B 成立的证明是一个函数,将 A 成立的证明转换成 B 成立的证明。

换句话说,如果知道 A → BA 同时成立,那么我们可以推出 B 成立:

→-elim :  {A B : Set}
   (A  B)
   A
    -------
   B
→-elim L M = L M

在中世纪,这条规则被叫做 modus ponens,它与函数应用相对应。

定义一个函数,不管是带名字的定义或是使用 Lambda 抽象,被称为引入一个函数, 使用一个函数被称为消去一个函数。

引入后接着消去,得到的还是原来的值:

η-→ :  {A B : Set} (f : A  B)   (x : A)  f x)  f
η-→ f = refl

蕴涵比其他的运算符结合得都不紧密。因此 A ⊎ B → B ⊎ A 被解析为 (A ⊎ B) → (B ⊎ A)

给定两个类型 AB,我们将 A → B 称为从 AB函数空间。 它有时也被称作以 B 为底,A 为次数的。如果类型 Am 个不同的成员, 类型 Bn 个不同的成员,那么类型 A → Bnᵐ 个不同的成员。 这也是它被称为幂的原因之一。例如,考虑有两个成员的 Bool 类型,和有三个成员的 Tri 类型, 如之前的定义。那么,Bool → Tri 类型有如下的九个成员(三的平方):

λ{true → aa; false → aa}  λ{true → aa; false → bb}  λ{true → aa; false → cc}
λ{true → bb; false → aa}  λ{true → bb; false → bb}  λ{true → bb; false → cc}
λ{true → cc; false → aa}  λ{true → cc; false → bb}  λ{true → cc; false → cc}

下面的函数枚举了所有类型为 Bool → Tri 的参数:

→-count : (Bool  Tri)  
→-count f with f true | f false
...          | aa     | aa      =   1
...          | aa     | bb      =   2
...          | aa     | cc      =   3
...          | bb     | aa      =   4
...          | bb     | bb      =   5
...          | bb     | cc      =   6
...          | cc     | aa      =   7
...          | cc     | bb      =   8
...          | cc     | cc      =   9

类型上的幂与数的幂有相似的性质,很多数上成立的关系式也可以在类型上成立。

对应如下的定律:

(p ^ n) ^ m  ≡  p ^ (n * m)

我们有如下的同构:

A → (B → C)  ≃  (A × B) → C

两个类型可以被看作给定 A 成立的证据和 B 成立的证据,返回 C 成立的证据。 这个同构有时也被称作柯里化(Currying)。右逆的证明需要外延性:

currying :  {A B C : Set}  (A  B  C)  (A × B  C)
currying =
  record
    { to      =  λ{ f  λ{  x , y   f x y }}
    ; from    =  λ{ g  λ{ x  λ{ y  g  x , y  }}}
    ; from∘to =  λ{ f  refl }
    ; to∘from =  λ{ g  extensionality λ{  x , y   refl }}
    }

柯里化告诉我们,如果一个函数有取一个数据对作为参数, 那么我们可以构造一个函数,取第一个参数,返回一个取第二个参数,返回最终结果的函数。 因此,举例来说,下面表示加法的形式:

_+_ : ℕ → ℕ → ℕ

和下面的一个带有一个数据对作为参数的函数是同构的:

_+′_ : (ℕ × ℕ) → ℕ

Agda 对柯里化进行了优化,因此 2 + 3_+_ 2 3 的简写。在一个对有序对进行优化的语言里, 2 +′ 3 可能是 _+′_ ⟨ 2 , 3 ⟩ 的简写。

对应如下的定律:

p ^ (n + m) = (p ^ n) * (p ^ m)

我们有如下的同构:

(A ⊎ B) → C  ≃  (A → C) × (B → C)

命题如果 A 成立或者 B 成立,那么 C 成立,和命题如果 A 成立,那么 C 成立以及 如果 B 成立,那么 C 成立,是一样的。左逆的证明需要外延性:

→-distrib-⊎ :  {A B C : Set}  (A  B  C)  ((A  C) × (B  C))
→-distrib-⊎ =
  record
    { to      = λ{ f   f  inj₁ , f  inj₂  }
    ; from    = λ{  g , h   λ{ (inj₁ x)  g x ; (inj₂ y)  h y } }
    ; from∘to = λ{ f  extensionality λ{ (inj₁ x)  refl ; (inj₂ y)  refl } }
    ; to∘from = λ{  g , h   refl }
    }

对应如下的定律:

(p * n) ^ m = (p ^ m) * (n ^ m)

我们有如下的同构:

A → B × C  ≃  (A → B) × (A → C)

命题如果 A 成立,那么 B 成立和 C 成立,和命题如果 A 成立,那么 B 成立以及 如果 A 成立,那么 C 成立,是一样的。左逆的证明需要外延性和积的 η-× 规则:

→-distrib-× :  {A B C : Set}  (A  B × C)  (A  B) × (A  C)
→-distrib-× =
  record
    { to      = λ{ f   proj₁  f , proj₂  f  }
    ; from    = λ{  g , h   λ x   g x , h x  }
    ; from∘to = λ{ f  extensionality λ{ x  η-× (f x) } }
    ; to∘from = λ{  g , h   refl }
    }

分配律

在同构意义下,积对于和满足分配律。验证这条形式的代码和之前的证明相似:

×-distrib-⊎ :  {A B C : Set}  (A  B) × C  (A × C)  (B × C)
×-distrib-⊎ =
  record
    { to      = λ{  inj₁ x , z   (inj₁  x , z )
                 ;  inj₂ y , z   (inj₂  y , z )
                 }
    ; from    = λ{ (inj₁  x , z )   inj₁ x , z 
                 ; (inj₂  y , z )   inj₂ y , z 
                 }
    ; from∘to = λ{  inj₁ x , z   refl
                 ;  inj₂ y , z   refl
                 }
    ; to∘from = λ{ (inj₁  x , z )  refl
                 ; (inj₂  y , z )  refl
                 }
    }

和对于积不满足分配律,但满足嵌入:

⊎-distrib-× :  {A B C : Set}  (A × B)  C  (A  C) × (B  C)
⊎-distrib-× =
  record
    { to      = λ{ (inj₁  x , y )   inj₁ x , inj₁ y 
                 ; (inj₂ z)           inj₂ z , inj₂ z 
                 }
    ; from    = λ{  inj₁ x , inj₁ y   (inj₁  x , y )
                 ;  inj₁ x , inj₂ z   (inj₂ z)
                 ;  inj₂ z , _        (inj₂ z)
                 }
    ; from∘to = λ{ (inj₁  x , y )  refl
                 ; (inj₂ z)          refl
                 }
    }

我们在定义 from 函数的时候可以有选择。给定的定义中,它将 ⟨ inj₂ z , inj₂ z′ ⟩ 转换为 inj₂ z,但我们也可以返回 inj₂ z′ 作为嵌入证明的变种。我们在这里只能证明嵌入, 而不能证明同构,因为 from 函数必须丢弃 z 或者 z′ 其中的一个。

在一般的逻辑学方法中,两条分配律都以等价的形式给出,每一边都蕴涵了另一边:

A × (B ⊎ C) ⇔ (A × B) ⊎ (A × C)
A ⊎ (B × C) ⇔ (A ⊎ B) × (A ⊎ C)

但当我们考虑提供上述蕴涵证明的函数时,第一条对应同构而第二条只能对应嵌入, 揭示了有些定理比另一个更加的「正确」。

练习 ⊎-weak-× (推荐)

证明如下性质成立:

postulate
  ⊎-weak-× :  {A B C : Set}  (A  B) × C  A  (B × C)

这被称为弱分配律(Weak Distributive Law)。给出相对应的分配律,并解释分配律与弱分配律的关系。

-- 请将代码写在此处。
练习 ⊎×-implies-×⊎(实践)

证明合取的析取蕴涵了析取的合取:

postulate
  ⊎×-implies-×⊎ :  {A B C D : Set}  (A × B)  (C × D)  (A  C) × (B  D)

反命题成立吗?如果成立,给出证明;如果不成立,给出反例。

-- 请将代码写在此处。

标准库

标准库中可以找到与本章节中相似的定义:

import Data.Product using (_×_; proj₁; proj₂) renaming (_,_ to ⟨_,_⟩)
import Data.Unit using (; tt)
import Data.Sum using (_⊎_; inj₁; inj₂) renaming ([_,_] to case-⊎)
import Data.Empty using (; ⊥-elim)
import Function.Equivalence using (_⇔_)

标准库中使用 _,_ 构造数据对,而我们使用 ⟨_,_⟩。前者在从数据对构造三元对或者更大的 元组时更加的方便,允许 a , b , c 作为 (a, (b , c)) 的记法。但它与其他有用的记法相冲突, 比如说 Lists 中的 [_,_] 记法表示两个元素的列表, 或者 DeBruijn 章节中的 Γ , A 来表示环境的扩展。 标准库中的 _⇔_ 和我们的相似,但使用起来比较不便, 因为它可以根据任意的相等性定义进行参数化。

Unicode

本章节使用下列 Unicode:

×  U+00D7  乘法符号 (\x)
⊎  U+228E  多重集并集 (\u+)
⊤  U+22A4  向下图钉 (\top)
⊥  U+22A5  向上图钉 (\bot)
η  U+03B7  希腊小写字母 ETA (\eta)
₁  U+2081  下标 1 (\_1)
₂  U+2082  下标 2 (\_2)
⇔  U+21D4  左右双箭头 (\<=>)

Negation: 直觉逻辑与命题逻辑中的否定

module plfa.part1.Negation where

本章介绍了否定的性质,讨论了直觉逻辑和经典逻辑。

Imports

open import Relation.Binary.PropositionalEquality using (_≡_; refl)
open import Data.Nat using (; zero; suc)
open import Data.Empty using (; ⊥-elim)
open import Data.Sum using (_⊎_; inj₁; inj₂)
open import Data.Product using (_×_)
open import plfa.part1.Isomorphism using (_≃_; extensionality)

否定

给定命题 A,当 A 不成立时,它的否定形式 ¬ A 成立。 我们将否定阐述为「蕴涵假」来形式化此概念。

¬_ : Set  Set
¬ A = A  

这是归谬法(Reductio ad Absurdum)的一种形式:若从 A 可得出结论 (即谬误), 则 ¬ A 必定成立。

¬ A 成立的证据的形式为:

λ{ x → N }

其中 N 是类型为 的项,它包含类型为 A 的自由变量 x。换言之,¬ A 成立 的证据是一个函数,该函数将 A 成立的证据转换为 成立的证据。

给定 ¬ AA 均成立的证据,我们可以得出 成立。换言之,若 ¬ AA 均成立, 那么我们就得到了矛盾:

¬-elim :  {A : Set}
   ¬ A
   A
    ---
   
¬-elim ¬x x = ¬x x

在这里,我们将 ¬ A 的证据写作 ¬x,将 A 的证据写作 x。这表示 ¬x 必须是类型为 A → ⊥ 的函数,因此应用 ¬x x 得到的类型必为 。注意此规则只是 →-elim 的一个特例。

我们将否定的优先级设定为高于析取和合取,但低于其它运算:

infix 3 ¬_

因此,¬ A × ¬ B 会解析为 (¬ A) × (¬ B),而 ¬ m ≡ n 会解析为 ¬ (m ≡ n)

经典逻辑中,A 等价于 ¬ ¬ A。而如前文所述,Agda 中使用了直觉逻辑, 因此我们只有该等价关系的一半,即 A 蕴涵 ¬ ¬ A

¬¬-intro :  {A : Set}
   A
    -----
   ¬ ¬ A
¬¬-intro x  =  λ{¬x  ¬x x}

xA 的证据。我们要证明若假定 ¬ A 成立,则会导出矛盾,因此 ¬ ¬ A 必定成立。令 ¬x¬ A 的证据。那么以 ¬x x 为证据,从 A¬ A 可以导出矛盾。 这样我们就证明了 ¬ ¬ A

以上描述的等价写法如下:

¬¬-intro′ :  {A : Set}
   A
    -----
   ¬ ¬ A
¬¬-intro′ x ¬x = ¬x x

在这里我们简单地将 λ-项的参数转换成了该函数的额外参数。 我们通常会使用后面这种形式,因为它更加紧凑。

我们无法证明 ¬ ¬ A 蕴涵 A,但可以证明 ¬ ¬ ¬ A 蕴涵 ¬ A

¬¬¬-elim :  {A : Set}
   ¬ ¬ ¬ A
    -------
   ¬ A
¬¬¬-elim ¬¬¬x  =  λ x  ¬¬¬x (¬¬-intro x)

¬¬¬x¬ ¬ ¬ A 的证据。我们要证明若假定 A 成立就会导出矛盾, 因此 ¬ A 必定成立。令 xA 的证据。根据前面的结果,以 ¬¬-intro x 为证据可得出结论 ¬ ¬ A。根据 ¬¬¬x (¬¬-intro x),我们可从 ¬ ¬ ¬ A¬ ¬ A 导出矛盾。这样我们就证明了 ¬ A

另一个逻辑规则是换质换位律(contraposition),它陈述了若 A 蕴涵 B, 则 ¬ B 蕴涵 ¬ A

contraposition :  {A B : Set}
   (A  B)
    -----------
   (¬ B  ¬ A)
contraposition f ¬y x = ¬y (f x)

fA → B 的证据,¬y¬ B 的证据。我们要证明,若假定 A 成立就会导出矛盾,因此 ¬ A 必定成立。令 xA 的证据。根据 f x, 我们可从 A → BA 我们可得出结论 B。而根据 ¬y (f x),可从 B¬ B 得出结论 。这样,我们就证明了 ¬ A

利用否定可直接定义不等性:

_≢_ :  {A : Set}  A  A  Set
x  y  =  ¬ (x  y)

要证明不同的数不相等很简单:

_ : 1  2
_ = λ()

这是我们第一次在 λ-表达式中使用谬模式(Absurd Pattern)。类型 M ≡ N 只有在 MN 可被化简为相同的项时才能居留。由于 12 会化简为不同的正规形式,因此 Agda 判定没有证据可证明 1 ≡ 2。 第二个例子是,很容易验证皮亚诺公理中「零不是任何数的后继数」的假设:

peano :  {m : }  zero  suc m
peano = λ()

它们的证明基本上相同,因为谬模式会匹配所有类型为 zero ≡ suc m 的可能的证据。

鉴于蕴涵和幂运算之间的对应关系,以及没有成员的类型为假, 我们可以将否定看作零的幂。它确实对应于我们所知的算术运算,即

0 ^ n  ≡  1,  if n ≡ 0
       ≡  0,  if n ≢ 0

确实,只有一个 ⊥ → ⊥ 的证明。我们可以用两种方式写出此证明:

id :   
id x = x

id′ :   
id′ ()

不过使用外延性,我们可以证明二者相等:

id≡id′ : id  id′
id≡id′ = extensionality (λ())

根据外延性,对于任何在二者定义域中的 x,都有 id x ≡ id′ x, 则 id ≡ id′ 成立。不过没有 x 在它们的定义域中,因此其相等性平凡成立。

实际上,我们可以证明任意两个否定的证明都是相等的:

assimilation :  {A : Set} (¬x ¬x′ : ¬ A)  ¬x  ¬x′
assimilation ¬x ¬x′ = extensionality  x  ⊥-elim (¬x x))

¬ A 的证据蕴涵任何 A 的证据都可直接得出矛盾。但由于外延性全称量化了使 A 成立的 x,因此任何这样的 x 都会直接导出矛盾,同样其相等性平凡成立。

练习 <-irreflexive(推荐)

利用否定证明严格不等性满足非自反性, 即 n < n 对于任何 n 都不成立。

-- 请将代码写在此处
练习 trichotomy(实践)

请证明严格不等性满足三分律, 即对于任何自然数 mn,以下三条刚好只有一条成立:

  • m < n
  • m ≡ n
  • m > n

「刚好只有一条」的意思是,三者中不仅有一条成立,而且当其中一条成立时, 其它二者的否定也必定成立。

-- 请将代码写在此处
练习 ⊎-dual-×(推荐)

请证明合取、析取和否定可通过以下版本的德摩根定律(De Morgan’s Law)关联在一起。

¬ (A ⊎ B) ≃ (¬ A) × (¬ B)

此结果是我们之前证明的定理的简单推论。

-- 请将代码写在此处

以下命题也成立吗?

¬ (A × B) ≃ (¬ A) ⊎ (¬ B)

若成立,请证明;若不成立,你能给出一个比同构更弱的关系将两边关联起来吗?

直觉逻辑与经典逻辑

在 Gilbert 和 Sullivan 的电影《船夫》(The Gondoliers)中, Casilda 被告知她还是个婴儿时,就被许配给了巴塔维亚国王的继承人。 但由于一场动乱,没人知道她被许配给了两位继承人 Marco 和 Giuseppe 中的哪一位。她惊慌地哀嚎道:「那么你的意思是说我嫁给了两位船夫中的一位, 但却无法确定是谁?」对此的回答是:「虽然不知道是谁,但这件事却是毫无疑问的。」

逻辑学有很多变种,而经典逻辑直觉逻辑之间有一个区别。 直觉主义者关注于某些逻辑学家对无限性本质的假设,坚持真理的构造主义的概念。 具体来说,它们坚持认为 A ⊎ B 的证明必须确定 AB 中的哪一个成立, 因此它们会解决宣称 Casilda 嫁给了 Marco 或者 Giuseppe,直到其中一个被确定为 她的丈夫为止。或许 Gilbert 和 Sullivan 期待直觉主义,因为在故事的结局中, 继承人是第三个人 Luiz,他和 Casilda 已经顺利地相爱了。

直觉主义者也拒绝排中律(Law of the Excluded Middle)————该定律断言,对于所有的 AA ⊎ ¬ A 必定成立————因为该定律没有给出 A¬ A 中的哪一个成立。 海廷(Heyting)形式化了希尔伯特(Hilbert)经典逻辑的一个变种,抓住了直觉主义中可证明性的概念。 具体来说,排中律在希尔伯特逻辑中是可证明的,但在海廷逻辑中却不可证明。 进一步来说,如果排中律作为一条公理添加到海廷逻辑中,那么它会等价于希尔伯特逻辑。 柯尔莫哥洛夫(Kolmogorov)证明了两种逻辑紧密相关:他给出了双重否定翻译,即一个式子在经典逻辑中 可证,当且仅当它的双重否定式在直觉逻辑中可证。

「命题即类型」最初是为直觉逻辑而制定的。这是一种完美的契合,因为在直觉主义的 解释中,式子 A ⊎ B 刚好可以在给出 AB 之一的证明时得证,因此对应于析取 的类型是一个不交和(Disjoint Sum)。

(以上内容部分取自 “Propositions as Types”, Philip Wadler, Communications of the ACM,2015 年 12 月。)

排中律是不可辩驳的

排中律可形式化如下:

postulate
  em :  {A : Set}  A  ¬ A

如之前所言,排中律在直觉逻辑中并不成立。然而,我们可以证明它是 不可辩驳(Irrefutable)的,即其否定的否定是可证明的(因而其否定式不可证明):

em-irrefutable :  {A : Set}  ¬ ¬ (A  ¬ A)
em-irrefutable = λ k  k (inj₂  x  k (inj₁ x)))

解释此代码的最佳方式是交互式地推导它:

em-irrefutable k = ?

给定 ¬ (A ⊎ ¬ A) 的证据 k,即一个函数,它接受一个类型为 A ⊎ ¬ A 的值, 返回一个空类型的值,我们必须在 ? 处填上一个返回空类型的项。得到空类型值 的唯一方式就是应用 k 本身,于是我们据此展开此洞:

em-irrefutable k = k ?

我们需要用类型为 A ⊎ ¬ A 的值填上这个新的洞。由于目前我们并没有类型为 A 的值, 因此先处理第二个析取:

em-irrefutable k = k (inj₂ λ{ x → ? })

第二个析取接受 ¬ A 的证据,即一个函数,它接受类型为 A 的值,返回空类型的值。 我们将 x 绑定到类型为 A 的值,现在我们需要在洞中填入空类型的值。同样, 得到空类型的值的唯一方法就是将 k 应用到其自身,于是我们展开此洞:

em-irrefutable k = k (inj₂ λ{ x → k ? })

这次我们就有一个类型为 A 的值了,其名为 x,于是我们可以处理第一个析取:

em-irrefutable k = k (inj₂ λ{ x → k (inj₁ x) })

现在没有洞了!这样就完成了证明。

下面的故事说明了我们创建的项的行为。 (向 Peter Selinger 道歉,他讲的是个关于国王,巫师和贤者之石的类似的故事。)

曾经有一个恶魔向一个男人提议:「要么 (a) 我给你 10 亿美元,要么 (b) 如果你付给我 10 亿美元,我可以实现你的任何一个愿望。当然,得是我决定提供 (a) 还是 (b)。」

男人很谨慎。他需要付出他的灵魂吗? 恶魔说不用,他只要接受这个提议就行。

于是男人思索着,如果恶魔向他提供 (b),那么他不太可能付得起这个愿望。 不过倘若真是如此的话,能有什么坏处吗?

「我接受」,男人回答道,「我能得到 (a) 还是 (b)?」

恶魔顿了顿。「我提供 (b)。」

男人很失望,但并不惊讶。「果然是这样」,他想。 但是这个提议折磨着他。想想他都能用这个愿望做些什么! 多年以后,男人开始积累钱财。为了得到这笔钱,他有时会做坏事, 而且他隐约意识到这一定是魔鬼所想到的。最后他攒够了 10 亿美元,恶魔再次出现了。

「这是 10 亿美元」,男人说着,交出一个手提箱。「实现我的愿望吧!」

恶魔接过了手提箱。然后他说道,「哦?我之前说的是 (b) 吗?抱歉,我说的是 (a)。 很高兴能给你 10 亿美元。」

于是恶魔将那个手提箱又还给了他。

(以上内容部分取自 “Call-by-Value is Dual to Call-by-Name”, Philip Wadler, International Conference on Functional Programming, 2003 年。)

练习 Classical(延伸)

考虑以下定律:

  • 排中律:对于所有 AA ⊎ ¬ A
  • 双重否定消去:对于所有的 A¬ ¬ A → A
  • 皮尔士定律:对于所有的 AB((A → B) → A) → A
  • 蕴涵表示为析取:对于所有的 AB(A → B) → ¬ A ⊎ B
  • 德摩根定律:对于所有的 AB¬ (¬ A × ¬ B) → A ⊎ B

请证明其中任意一条定律都蕴涵其它所有定律。

-- 请将代码写在此处
联系 Stable(延伸)

若双重否定消去对某个式子成立,我们就说它是稳定(Stable)的:

Stable : Set  Set
Stable A = ¬ ¬ A  A

请证明任何否定式都是稳定的,并且两个稳定式的合取也是稳定的。

-- 请将代码写在此处

标准库

本章中的类似定义可在标准库中找到:

import Relation.Nullary using (¬_)
import Relation.Nullary.Negation using (contraposition)

Unicode

本章使用了以下 Unicode:

¬  U+00AC  否定符号 (\neg)
≢  U+2262  不等价于 (\==n)

Quantifiers: 全称量词与存在量词

module plfa.part1.Quantifiers where

本章节介绍全称量化(Universal Quantification)和存在量化(Existential Quantification)。

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl)
open import Data.Nat using (; zero; suc; _+_; _*_)
open import Relation.Nullary using (¬_)
open import Data.Product using (_×_; proj₁; proj₂) renaming (_,_ to ⟨_,_⟩)
open import Data.Sum using (_⊎_; inj₁; inj₂)
open import plfa.part1.Isomorphism using (_≃_; extensionality; ∀-extensionality)
open import Function using (_∘_)

全称量化

我们用依赖函数类型(Dependent Function Type)来形式化全称量化, 这样的形式在书中贯穿始终。例如,在归纳一章中,我们证明了加法满足结合律:

+-assoc : ∀ (m n p : ℕ) → (m + n) + p ≡ m + (n + p)

它断言对于所有的自然数 mnp(m + n) + p ≡ m + (n + p) 成立。 它是一个依赖函数,给出 mnp 的值,它会返回与该等式对应的证据。

通常给定一个 A 类型的变量 x 和一个带有 x 自由变量的命题 B x,全称量化 的命题 ∀ (x : A) → B x 当对于所有类型为 A 的项 M,命题 B M 成立时成立。 在这里,B M 代表了将 B x 中自由出现的变量 x 替换成 M 后的命题。 变量 xB x 中以自由变量的形式出现,但是在 ∀ (x : A) → B x 中是约束的。

∀ (x : A) → B x 成立的证明由以下形式构成:

λ (x : A) → N x

其中 N x 是一个 B x 类型的项,N xB x 都包含了一个 A 类型的自由变量 x。 给定一个项 L, 其提供 ∀ (x : A) → B x 成立的证明,和一个类型为 A 的项 ML M 这一项则是 B M 成立的证明。换句话说,∀ (x : A) → B x 成立的证明是一个函数, 将类型为 A 的项 M 转换成 B M 成立的证明。

再换句话说,如果我们知道 ∀ (x : A) → B x 成立,又知道 M 是一个类型为 A 的项, 那么我们可以推导出 B M 成立:

∀-elim :  {A : Set} {B : A  Set}
   (L :  (x : A)  B x)
   (M : A)
    -----------------
   B M
∀-elim L M = L M

→-elim 那样,这条规则对应了函数应用。

函数是依赖函数的一种特殊形式,其值域不取决于定义域中的变量。当一个函数被视为 蕴涵的证明时,它的参数和结果都是证明,而当一个依赖函数被视为全称量词的证明时, 它的参数被视为数据类型中的一个元素,而结果是一个依赖于参数的命题的证明。因为在 Agda 中,一个数据类型中的一个值和一个命题的证明是无法区别的,这样的区别很大程度上 取决于如何来诠释。

依赖函数类型也被叫做依赖积(Dependent Product),因为如果 A 是一个有限的数据类型, 有值 x₁ , ⋯ , xₙ,如果每个类型 B x₁ , ⋯ , B xₙm₁ , ⋯ , mₙ 个不同的成员, 那么 ∀ (x : A) → B xm₁ * ⋯ * mₙ 个成员。的确,∀ (x : A) → B x 的记法有时 也被 Π[ x ∈ A ] (B x) 取代,其中 Π 代表积。然而,我们还是使用依赖函数这个名称, 因为依赖积这个名称是有歧义的,我们后续会体会到歧义所在。

练习 ∀-distrib-× (推荐)

证明全称量词对于合取满足分配律:

postulate
  ∀-distrib-× :  {A : Set} {B C : A  Set} 
    (∀ (x : A)  B x × C x)  (∀ (x : A)  B x) × (∀ (x : A)  C x)

将这个结果与 Connectives 章节中的 (→-distrib-×) 结果对比。

提示:你需要 ∀-extensionality

练习 ⊎∀-implies-∀⊎(实践)

证明全称命题的析取蕴涵了析取的全称命题:

postulate
  ⊎∀-implies-∀⊎ :  {A : Set} {B C : A  Set} 
    (∀ (x : A)  B x)  (∀ (x : A)  C x)   (x : A)  B x  C x

逆命题成立么?如果成立,给出证明。如果不成立,解释为什么。

练习 ∀-×(实践)

参考下面的类型:

data Tri : Set where
  aa : Tri
  bb : Tri
  cc : Tri

B 作为由 Tri 索引的一个类型,也就是说 B : Tri → Set。 证明 ∀ (x : Tri) → B xB aa × B bb × B cc 是同构的。

Hint: you will need to use ∀-extensionality.

提示:你需要 ∀-extensionality

存在量化

给定一个 A 类型的变量 x 和一个带有 x 自由变量的命题 B x,存在量化 的命题 Σ[ x ∈ A ] B x 当对于一些类型为 A 的项 M,命题 B M 成立时成立。 在这里,B M 代表了将 B x 中自由出现的变量 x 替换成 M 以后的命题。 变量 xB x 中以自由变量形式出现,但是在 Σ[ x ∈ A ] B x 中是约束的。

我们定义一个合适的归纳数据类型来形式化存在量化:

data Σ (A : Set) (B : A  Set) : Set where
  ⟨_,_⟩ : (x : A)  B x  Σ A B

我们为存在量词定义一个方便的语法:

Σ-syntax = Σ
infix 2 Σ-syntax
syntax Σ-syntax A  x  Bx) = Σ[ x  A ] Bx

这是我们第一次使用语法声明,其表示左手边的项可以以右手边的语法来书写。 这种特殊语法只有在标识符 Σ-syntax 被导入时可用。

Σ[ x ∈ A ] B x 成立的证明由 ⟨ M , N ⟩ 组成,其中 M 是类型为 A 的项, NB M 成立的证明。

我们也可以用记录类型来等价地定义存在量化。

record Σ′ (A : Set) (B : A  Set) : Set where
  field
    proj₁′ : A
    proj₂′ : B proj₁′

这里的记录构造

record
  { proj₁′ = M
  ; proj₂′ = N
  }

对应了项

⟨ M , N ⟩

其中 M 是类型为 A 的项,N 是类型为 B M 的项。

积是存在量词的一种特殊形式,其第二分量不取决于第一分量中的变量。当一个积被视为 合取的证明时,它的两个分量都是证明,而当一个依赖积被视为存在量词的证明时, 它的第一分量被视为数据类型中的一个元素,而第二分量是一个依赖于第一分量的命题的证明。因为在 Agda 中,一个数据类型中的一个值一个命题的证明是无法区别的,这样的区别很大程度上 取决于如何来诠释。

存在量化也被叫做依赖和(Dependent Sum),因为如果 A 是一个有限的数据类型, 有值 x₁ , ⋯ , xₙ,如果每个类型 B x₁ , ⋯ , B xₙm₁ , ⋯ , mₙ 个不同的成员, 那么 Σ[ x ∈ A ] B xm₁ + ⋯ + mₙ 个成员,这也解释了选择使用这个记法的原因—— Σ 代表和。

存在量化有时也被叫做依赖积(Dependent Product),因为积是其中的一种特殊形式。但是, 这样的叫法非常让人困扰,因为全程量化也被叫做依赖积,而存在量化已经有依赖和的叫法。

存在量词的普通记法是 (与全程量词的 记法相类似)。我们使用 Agda 标准库中的惯例, 使用一种隐式申明约束变量定义域的记法。

 :  {A : Set} (B : A  Set)  Set
 {A} B = Σ A B

∃-syntax = 
syntax ∃-syntax  x  B) = ∃[ x ] B

这种特殊的语法只有在 ∃-syntax 标识符被导入时可用。我们将倾向于使用这种语法,因为它更短, 而且看上去更熟悉。

给定 ∀ x → B x → C 成立的证明,其中 C 不包括自由变量 x,给定 ∃[ x ] B x 成立的 证明,我们可以推导出 C 成立。

∃-elim :  {A : Set} {B : A  Set} {C : Set}
   (∀ x  B x  C)
   ∃[ x ] B x
    ---------------
   C
∃-elim f  x , y  = f x y

换句话说,如果我们知道对于任何 A 类型的 xB x 蕴涵了 C,还知道对于某个类型 AxB x 成立,那么我们可以推导出 C 成立。这是因为我们可以先将 ∀ x → B x → C 的证明对于 A 类型的 xB x 类型的 y 实例化,而这样的值恰好可以由 ∃[ x ] B x 的证明来提供。

的确,逆命题也成立,两者合起来构成一个同构:

∀∃-currying :  {A : Set} {B : A  Set} {C : Set}
   (∀ x  B x  C)  (∃[ x ] B x  C)
∀∃-currying =
  record
    { to      =  λ{ f  λ{  x , y   f x y }}
    ; from    =  λ{ g  λ{ x  λ{ y  g  x , y  }}}
    ; from∘to =  λ{ f  refl }
    ; to∘from =  λ{ g  extensionality λ{  x , y   refl }}
    }

这可以被看做是将柯里化推广的结果。的确,建立这两者同构的证明与之前我们讨论 蕴涵时给出的证明是一样的。

练习 ∃-distrib-⊎ (推荐)

证明存在量词对于析取满足分配律:

postulate
  ∃-distrib-⊎ :  {A : Set} {B C : A  Set} 
    ∃[ x ] (B x  C x)  (∃[ x ] B x)  (∃[ x ] C x)
练习 ∃×-implies-×∃(实践)

证明合取的存在命题蕴涵了存在命题的合取:

postulate
  ∃×-implies-×∃ :  {A : Set} {B C : A  Set} 
    ∃[ x ] (B x × C x)  (∃[ x ] B x) × (∃[ x ] C x)

逆命题成立么?如果成立,给出证明。如果不成立,解释为什么。

练习 ∃-⊎(实践)

沿用练习 ∀-× 中的 TriB 。 证明 ∃[ x ] B xB aa ⊎ B bb ⊎ B cc 是同构的。

一个存在量化的例子

回忆我们在 Relations 章节中定义的 evenodd

data even :   Set
data odd  :   Set

data even where

  even-zero : even zero

  even-suc :  {n : }
     odd n
      ------------
     even (suc n)

data odd where
  odd-suc :  {n : }
     even n
      -----------
     odd (suc n)

如果一个数是 0 或者它是奇数的后继,那么这个数是偶数。如果一个数是偶数的后继,那么这个数是奇数。

我们接下来要证明,一个数是偶数当且仅当这个数是一个数的两倍,一个数是奇数当且仅当这个数 是一个数的两倍多一。换句话说,我们要证明的是:

even n 当且仅当 ∃[ m ] ( m * 2 ≡ n)

odd n 当且仅当 ∃[ m ] (1 + m * 2 ≡ n)

惯例来说,我们往往将常数因子写在前面、将和里的常数项写在后面。但是这里我们没有按照惯例, 而是反了过来,因为这样可以让证明更简单:

这是向前方向的证明:

even-∃ :  {n : }  even n  ∃[ m ] (    m * 2  n)
odd-∃  :  {n : }   odd n  ∃[ m ] (1 + m * 2  n)

even-∃ even-zero                       =   zero , refl 
even-∃ (even-suc o) with odd-∃ o
...                    |  m , refl   =   suc m , refl 

odd-∃  (odd-suc e)  with even-∃ e
...                    |  m , refl   =   m , refl 

我们定义两个相互递归的函数。给定 n 是奇数或者是偶数的证明,我们返回一个数 m,以及 m * 2 ≡ n 或者 1 + m * 2 ≡ n 的证明。我们根据 n 是奇数 或者是偶数的证明进行归纳:

  • 如果这个数是偶数,因为它是 0,那么我们返回数据对 0 ,以及 0 的两倍是 0 的证明。

  • 如果这个数是偶数,因为它是比一个奇数多 1,那么我们可以使用归纳假设,来获得一个数 m1 + m * 2 ≡ n 的证明。我们返回数据对 suc m 以及 suc m * 2 ≡ suc n 的证明—— 我们可以直接通过替换 n 来得到证明。

  • 如果这个数是奇数,因为它是一个偶数的后继,那么我们可以使用归纳假设,来获得一个数 mm * 2 ≡ n 的证明。我们返回数据对 m 以及 1 + m * 2 ≡ suc n 的证明—— 我们可以直接通过替换 n 来得到证明。

这样,我们就完成了正方向的证明。

接下来是反方向的证明:

∃-even :  {n : }  ∃[ m ] (    m * 2  n)  even n
∃-odd  :  {n : }  ∃[ m ] (1 + m * 2  n)   odd n

∃-even   zero , refl   =  even-zero
∃-even  suc m , refl   =  even-suc (∃-odd  m , refl )

∃-odd       m , refl   =  odd-suc (∃-even  m , refl )

给定一个是另一个数两倍的数,我们需要证明这个数是偶数。给定一个是另一个数两倍多一的数, 我们需要证明这个数是奇数。我们对于存在量化的证明进行归纳。在偶数的情况,我们也需要考虑两种 一个数是另一个数两倍的情况。

  • 在偶数是 zero 的情况中,我们需要证明 zero * 2 是偶数,由 even-zero 可得。

  • 在偶数是 suc n 的情况中,我们需要证明 suc m * 2 是偶数。归纳假设告诉我们, 1 + m * 2 是奇数,那么所求证的结果由 even-suc 可得。

  • 在偶数的情况中,我们需要证明 1 + m * 2 是奇数。归纳假设告诉我们,m * 2 是偶数, 那么所求证的结果由 odd-suc 可得。

这样,我们就完成了向后方向的证明。

练习 ∃-even-odd(实践)

如果我们用 2 * m 代替 m * 22 * m + 1 代替 1 + m * 2,上述证明会变得复杂多少呢? 用这种方法来重写 ∃-even∃-odd

-- 请将代码写在此处。
练习 ∃-+-≤(实践)

证明当且仅当存在一个 x 使得 x + y ≡ z 成立时 y ≤ z 成立。

-- 请将代码写在此处。

存在量化、全称量化和否定

存在量化的否定与否定的全称量化是同构的。考虑到存在量化是解构的推广,全称量化是合构的推广, 这样的结果与解构的否定与否定的合构是同构的结果相似。

¬∃≃∀¬ :  {A : Set} {B : A  Set}
   (¬ ∃[ x ] B x)   x  ¬ B x
¬∃≃∀¬ =
  record
    { to      =  λ{ ¬∃xy x y  ¬∃xy  x , y  }
    ; from    =  λ{ ∀¬xy  x , y   ∀¬xy x y }
    ; from∘to =  λ{ ¬∃xy  extensionality λ{  x , y   refl } }
    ; to∘from =  λ{ ∀¬xy  refl }
    }

to 的方向,给定了一个 ¬ ∃[ x ] B x 类型的值 ¬∃xy,需要证明给定一个 x 的值, 可以推导出 ¬ B x。换句话说,给定一个 B x 类型的值 y,我们可以推导出假。将 xy 合并起来我们就得到了 ∃[ x ] B x 类型的值 ⟨ x , y ⟩,对其使用 ¬∃xy 即可获得矛盾。

from 的方向,给定了一个 ∀ x → ¬ B x 类型的值 ∀¬xy,需要证明从一个类型为 ∃[ x ] B x 类型的值 ⟨ x , y ⟩ ,我们可以推导出假。将 ∀¬xy 使用于 x 之上, 可以得到类型为 ¬ B x 的值,对其使用 y 即可获得矛盾。

两个逆的证明很直接,其中有一个方向需要外延性。

练习 ∃¬-implies-¬∀ (推荐)

证明否定的存在量化蕴涵了全称量化的否定:

postulate
  ∃¬-implies-¬∀ :  {A : Set} {B : A  Set}
     ∃[ x ] (¬ B x)
      --------------
     ¬ (∀ x  B x)

逆命题成立吗?如果成立,给出证明。如果不成立,解释为什么。

练习 Bin-isomorphism (延伸)

回忆在练习 BinBin-lawsBin-predicates 中, 我们定义了比特串数据类型 Bin 来表示自然数,并要求你定义了下列函数和谓词:

to   : ℕ → Bin
from : Bin → ℕ
Can  : Bin → Set

以及证明了下列性质:

from (to n) ≡ n

----------
Can (to n)

Can b
---------------
to (from b) ≡ b

使用上述,证明 ∃[ b ](Can b) 之间存在同构。

我们建议证明以下引理,它描述了对于给定的二进制数 bOne b 只有一个证明, Can b,也是如此。

≡One : ∀ {b : Bin} (o o′ : One b) → o ≡ o′

≡Can : ∀ {b : Bin} (c c′ : Can b) → c ≡ c′

Many of the alternatives for proving to∘from turn out to be tricky. However, the proof can be straightforward if you use the following lemma, which is a corollary of ≡Can.

proj₁≡→Can≡ : {c c′ : ∃[ b ] Can b} → proj₁ c ≡ proj₁ c′ → c ≡ c′
-- 请将代码写在此处。

标准库

标准库中可以找到与本章中相似的定义:

import Data.Product using (Σ; _,_; ; Σ-syntax; ∃-syntax)

Unicode

本章节使用下列 Unicode:

Π  U+03A0  大写希腊字母 PI (\Pi)
Σ  U+03A3  大写希腊字母 SIGMA (\Sigma)
∃  U+2203  存在 (\ex, \exists)

Decidable: 布尔值与判定过程

module plfa.part1.Decidable where

我们有两种不同的方式来表示关系:一是表示为由关系成立的证明(Evidence)所构成的数据类型; 二是表示为一个计算(Compute)关系是否成立的函数。在本章中,我们将探讨这两种方式之间的关系。 我们首先研究大家熟悉的布尔值(Boolean)记法,但是之后我们会发现,相较布尔值记法, 使用一种新的可判定性(Decidable)记法将会是更好的选择。

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl)
open Eq.≡-Reasoning
open import Data.Nat using (; zero; suc)
open import Data.Product using (_×_) renaming (_,_ to ⟨_,_⟩)
open import Data.Sum using (_⊎_; inj₁; inj₂)
open import Relation.Nullary using (¬_)
open import Relation.Nullary.Negation using ()
  renaming (contradiction to ¬¬-intro)
open import Data.Unit using (; tt)
open import Data.Empty using (; ⊥-elim)
open import plfa.part1.Relations using (_<_; z<s; s<s)
open import plfa.part1.Isomorphism using (_⇔_)

证据 vs 计算

回忆我们在 Relations 章节中将比较定义为一个归纳数据类型,其提供了一个数小于或等于另外一个数的证明:

infix 4 _≤_

data _≤_ :     Set where

  z≤n :  {n : }
      --------
     zero  n

  s≤s :  {m n : }
     m  n
      -------------
     suc m  suc n

举例来说,我们提供 2 ≤ 4 成立的证明,也可以证明没有 4 ≤ 2 成立的证明。

2≤4 : 2  4
2≤4 = s≤s (s≤s z≤n)

¬4≤2 : ¬ (4  2)
¬4≤2 (s≤s (s≤s ()))

() 的出现表明了没有 2 ≤ 0 成立的证明:z≤n 不能匹配(因为 2 不是 zero),s≤s 也不能匹配(因为 0 不能匹配 suc n)。

作为替代的定义,我们可以定义一个大家可能比较熟悉的布尔类型:

data Bool : Set where
  true  : Bool
  false : Bool

给定了布尔类型,我们可以定义一个两个数的函数在比较关系成立时来计算true, 否则计算出 false

infix 4 _≤ᵇ_

_≤ᵇ_ :     Bool
zero ≤ᵇ n       =  true
suc m ≤ᵇ zero   =  false
suc m ≤ᵇ suc n  =  m ≤ᵇ n

定义中的第一条与最后一条与归纳数据类型中的两个构造子相对应。因为对于任意的 m,不可能出现 suc m ≤ zero 的证明,我们使用中间一条定义来表示。 举个例子,我们可以计算 2 ≤ᵇ 4 成立,也可以计算 4 ≤ᵇ 2 不成立:

_ : (2 ≤ᵇ 4)  true
_ =
  begin
    2 ≤ᵇ 4
  ≡⟨⟩
    1 ≤ᵇ 3
  ≡⟨⟩
    0 ≤ᵇ 2
  ≡⟨⟩
    true
  

_ : (4 ≤ᵇ 2)  false
_ =
  begin
    4 ≤ᵇ 2
  ≡⟨⟩
    3 ≤ᵇ 1
  ≡⟨⟩
    2 ≤ᵇ 0
  ≡⟨⟩
    false
  

在第一种情况中,我们需要两步来将第一个参数降低到 0,再用一步来计算出真,这对应着我们需要 使用两次 s≤s 和一次 z≤n 来证明 2 ≤ 4。 在第二种情况中,我们需要两步来将第二个参数降低到 0,再用一步来计算出假,这对应着我们需要 使用两次 s≤s 和一次 () 来说明没有 4 ≤ 2 的证明。

将证明与计算相联系

我们希望能够证明这两种方法是有联系的,而我们的确可以。 首先,我们定义一个函数来把计算世界映射到证明世界:

T : Bool  Set
T true   =  
T false  =  

回忆到 是只有一个元素 tt 的单元类型, 是没有值的空类型。(注意 T 是大写字母 t, 与 不同。)如果 bBool 类型的,那么如果 b 为真,tt 可以提供 T b 成立的证明; 如果 b 为假,则不可能有 T b 成立的证明。

换句话说,T b 当且仅当 b ≡ true 成立时成立。在向前的方向,我们需要针对 b 进行情况分析:

T→≡ :  (b : Bool)  T b  b  true
T→≡ true tt   =  refl
T→≡ false ()

如果 b 为真,那么 T btt 证明,b ≡ truerefl 证明。 当 b 为假,那么 T b 无法证明。

在向后的方向,不需要针对布尔值 b 的情况分析:

≡→T :  {b : Bool}  b  true  T b
≡→T refl  =  tt

如果 b ≡ truerefl 证明,我们知道 btrue,因此 T btt 证明。

现在我们可以证明 T (m ≤ᵇ n) 当且仅当 m ≤ n 成立时成立。

在向前的方向,我们考虑 _≤ᵇ_ 定义中的三条语句:

≤ᵇ→≤ :  (m n : )  T (m ≤ᵇ n)  m  n
≤ᵇ→≤ zero    n       tt  =  z≤n
≤ᵇ→≤ (suc m) zero    ()
≤ᵇ→≤ (suc m) (suc n) t   =  s≤s (≤ᵇ→≤ m n t)

第一条语句中,我们立即可以得出 zero ≤ᵇ n 为真,所以 T (m ≤ᵇ n)tt 而得, 相对应地 m ≤ nz≤n 而证明。在中间的语句中,我们立刻得出 suc m ≤ᵇ zero 为假,则 T (m ≤ᵇ n) 为空,因此我们无需证明 m ≤ n,同时也不存在这样的证明。在最后的语句中,我们对于 suc m ≤ᵇ suc n 递归至 m ≤ᵇ n。令 tT (suc m ≤ᵇ suc n) 的证明,如果其存在。 根据 _≤ᵇ_ 的定义,这也是 T (m ≤ᵇ n) 的证明。我们递归地应用函数来获得 m ≤ n 的证明,再使用 s≤s 将其转换成为 suc m ≤ suc n 的证明。

在向后的方向,我们考虑 m ≤ n 成立证明的可能形式:

≤→≤ᵇ :  {m n : }  m  n  T (m ≤ᵇ n)
≤→≤ᵇ z≤n        =  tt
≤→≤ᵇ (s≤s m≤n)  =  ≤→≤ᵇ m≤n

如果证明是 z≤n,我们立即可以得到 zero ≤ᵇ n 为真,所以 T (m ≤ᵇ n)tt 证明。 如果证明是 s≤s 作用于 m≤n,那么 suc m ≤ᵇ suc n 规约到 m ≤ᵇ n,我们可以递归地使用函数 来获得 T (m ≤ᵇ n) 的证明。

向前方向的证明比向后方向的证明多一条语句,因为在向前方向的证明中我们需要考虑比较结果为真和假 的语句,而向后方向的证明只需要考虑比较成立的语句。这也是为什么我们比起计算的形式,更加偏爱证明的形式, 因为这样让我们做更少的工作:我们只需要考虑关系成立时的情况,而可以忽略不成立的情况。

从另一个角度来说,有时计算的性质可能正是我们所需要的。面对一个大数值上的非显然关系, 使用电脑来计算出答案可能会更加方便。幸运的是,比起在证明计算之中犹豫, 我们有一种更好的方法来兼取其优。

取二者之精华

一个返回布尔值的函数提供恰好一比特的信息:这个关系成立或是不成立。相反地,证明的形式告诉我们 为什么这个关系成立,但却需要我们自行完成这个证明。不过,我们其实可以简单地定义一个类型来取二者之精华。 我们把它叫做:Dec A,其中 Dec可判定的(Decidable)的意思。

data Dec (A : Set) : Set where
  yes :   A  Dec A
  no  : ¬ A  Dec A

正如布尔值,这个类型有两个构造子。一个 Dec A 类型的值要么是以 yes x 的形式,其中 x 提供 A 成立的证明,或者是以 no ¬x 的形式,其中 x 提供了 A 无法成立的证明。(也就是说,¬x 是一个给定 A 成立的证据,返回矛盾的函数)

比如说,我们定义一个函数 _≤?_,给定两个数,判定是否一个数小于等于另一个,并提供证明来说明结论。

首先,我们使用两个有用的函数,用于构造不等式不成立的证明:

¬s≤z :  {m : }  ¬ (suc m  zero)
¬s≤z ()

¬s≤s :  {m n : }  ¬ (m  n)  ¬ (suc m  suc n)
¬s≤s ¬m≤n (s≤s m≤n) = ¬m≤n m≤n

第一个函数断言了 ¬ (suc m ≤ zero),由荒谬可得。因为每个不等式的成立证明必须是 zero ≤ n 或者 suc m ≤ suc n 的形式,两者都无法匹配 suc m ≤ zero。 第二个函数取 ¬ (m ≤ n) 的证明 ¬m≤n,返回 ¬ (suc m ≤ suc n) 的证明。 所有形如 suc m ≤ suc n 的证明必须是以 s≤s m≤n 的形式给出。因此我们可以构造一个 矛盾,以 ¬m≤n m≤n 来证明。

使用这些,我们可以直接的判定不等关系:

_≤?_ :  (m n : )  Dec (m  n)
zero  ≤? n                   =  yes z≤n
suc m ≤? zero                =  no ¬s≤z
suc m ≤? suc n with m ≤? n
...               | yes m≤n  =  yes (s≤s m≤n)
...               | no ¬m≤n  =  no (¬s≤s ¬m≤n)

_≤ᵇ_ 一样,定义有三条语句。第一条语句中,zero ≤ n 立即成立,由 z≤n 证明。 第二条语句中,suc m ≤ zero 立即不成立,由 ¬s≤z 证明。 第三条语句中,我们需要递归地应用 m ≤? n。有两种可能性,在 yes 的情况中,它会返回 m ≤ n 的证明 m≤n,所以 s≤s m≤n 即可作为 suc m ≤ suc n 的证明;在 no 的情况中, 它会返回 ¬ (m ≤ n) 的证明 ¬m≤n,所以 ¬s≤s ¬m≤n 即可作为 ¬ (suc m ≤ suc n) 的证明。

当我们写 _≤ᵇ_ 时,我们必须写两个其他的函数 ≤ᵇ→≤≤→≤ᵇ 来证明其正确性。 作为对比,_≤?_ 的定义自身就证明了其正确性,由类型即可得知。_≤?_ 的代码也比 _≤ᵇ_≤ᵇ→≤≤→≤ᵇ 加起来要简洁的多。我们稍后将会证明,如果我们需要后三者, 我们亦可简单地从 _≤?_ 中派生出来。

我们可以使用我们新的函数来计算出我们之前需要自己想出来的证明

_ : 2 ≤? 4  yes (s≤s (s≤s z≤n))
_ = refl

_ : 4 ≤? 2  no (¬s≤s (¬s≤s ¬s≤z))
_ = refl

你可以验证 Agda 的确计算出了这些值。输入 C-c C-n 并给出 2 ≤? 4 或者 4 ≤? 2 作为 需要的表达式,Agda 会输出如上的值。

(小细节:如果我们不把 ¬s≤z¬s≤s 作为顶层函数来定义,而是使用内嵌的匿名函数, Agda 可能会在规范化否定的证明中出现问题。)

练习 _<?_ (推荐)

与上面的函数相似,定义一个判定严格不等性的函数:

postulate
  _<?_ :  (m n : )  Dec (m < n)
-- 请将代码写在此处。
练习 _≡ℕ?_(实践)

定义一个函数来判定两个自然数是否相等。

postulate
  _≡ℕ?_ :  (m n : )  Dec (m  n)
-- 请将代码写在此处。

从可判定的值到布尔值,从布尔值到可判定的值

好奇的读者可能会思考能不能重用 m ≤ᵇ n 的定义,加上它与 m ≤ n 等价的证明, 来证明可判定性。的确,我们是可以做到的:

_≤?′_ :  (m n : )  Dec (m  n)
m ≤?′ n with m ≤ᵇ n | ≤ᵇ→≤ m n | ≤→≤ᵇ {m} {n}
...        | true   | p        | _            = yes (p tt)
...        | false  | _        | ¬p           = no ¬p

如果 m ≤ᵇ n 为真,那么 ≤ᵇ→≤ 会返回一个 m ≤ n 成立的证明。 如果 m ≤ᵇ n 为假,那么 ≤→≤ᵇ 会取一个 m ≤ n 成立的证明,将其转换为一个矛盾。

在这个证明中,with 语句的三重约束是必须的。如果我们取而代之的写:

_≤?″_ : ∀ (m n : ℕ) → Dec (m ≤ n)
m ≤?″ n with m ≤ᵇ n
... | true   =  yes (≤ᵇ→≤ m n tt)
... | false  =  no (≤→≤ᵇ {m} {n})

那么 Agda 对于每条语句会有一个抱怨:

⊤ !=< (T (m ≤ᵇ n)) of type Set
when checking that the expression tt has type T (m ≤ᵇ n)

T (m ≤ᵇ n) !=< ⊥ of type Set
when checking that the expression ≤→≤ᵇ {m} {n} has type ¬ m ≤ n

将表达式放在 with 语句中能让 Agda 利用下列事实:当 m ≤ᵇ n 为真时,T (m ≤ᵇ n);当 m ≤ᵇ n 为假时,T (m ≤ᵇ n)

然而,总体来说还是直接定义 _≤?_ 比较方便,正如之前部分中那样。如果有人真的很需要 _≤ᵇ_, 那么它和它的性质可以简单地从 _≤?_ 中派生出来,正如我们接下来要展示的一样。

擦除(Erasure)将一个可判定的值转换为一个布尔值:

⌊_⌋ :  {A : Set}  Dec A  Bool
 yes x   =  true
 no ¬x   =  false

使用擦除,我们可以简单地从 _≤?_ 中派生出 _≤ᵇ_

_≤ᵇ′_ :     Bool
m ≤ᵇ′ n  =   m ≤? n 
更进一步来说,如果 D 是一个类型为 Dec A 的值,那么 T ⌊ D ⌋ 当且仅当 A 成立时成立:
toWitness :  {A : Set} {D : Dec A}  T  D   A
toWitness {A} {yes x} tt  =  x
toWitness {A} {no ¬x} ()

fromWitness :  {A : Set} {D : Dec A}  A  T  D 
fromWitness {A} {yes x} _  =  tt
fromWitness {A} {no ¬x} x  =  ¬x x

使用这些,我们可以简单地派生出 T (m ≤ᵇ′ n) 当且仅当 m ≤ n 成立时成立。

≤ᵇ′→≤ :  {m n : }  T (m ≤ᵇ′ n)  m  n
≤ᵇ′→≤  =  toWitness

≤→≤ᵇ′ :  {m n : }  m  n  T (m ≤ᵇ′ n)
≤→≤ᵇ′  =  fromWitness

总结来说,最好避免直接使用布尔值,而使用可判定的值。如果有需要布尔值的时候,它们和它们的性质 可以简单地从对应的可判定的值中派生而来。

逻辑连接符

大多数读者对于布尔值的逻辑运算符很熟悉了。每个逻辑运算符都可以被延伸至可判定的值。

两个布尔值的合取当两者都为真时为真,当任一为假时为假:

infixr 6 _∧_

_∧_ : Bool  Bool  Bool
true   true  = true
false  _     = false
_      false = false

在 Emacs 中,第三个等式的左手边显示为灰色,表示这些等式出现的顺序决定了是第二条还是第三条 会被匹配到。然而,不管是哪一条被匹配到,结果都是一样的。

相应地,给定两个可判定的命题,我们可以判定它们的合取:

infixr 6 _×-dec_

_×-dec_ :  {A B : Set}  Dec A  Dec B  Dec (A × B)
yes x ×-dec yes y = yes  x , y 
no ¬x ×-dec _     = no λ{  x , y   ¬x x }
_     ×-dec no ¬y = no λ{  x , y   ¬y y }

两个命题的合取当两者都成立时成立,其否定则当任意一者否定成立时成立。如果两个都成立, 我们将每一证明放入数据对中,作为合取的证明。如果任意一者的否定成立,假设整个合取将会引入一个矛盾。

同样地,在 Emacs 中,第三条等式在左手边以灰色显示,说明等式的顺序决定了第二条还是第三条会被匹配。 这一次,我们给出的结果会因为是第二条还是第三条而不一样。如果两个命题都不成立,我们选择第一个来构造矛盾, 但选择第二个也是同样正确的。

两个布尔值的析取当任意为真时为真,当两者为假时为假:

infixr 5 _∨_

_∨_ : Bool  Bool  Bool
true   _      = true
_      true   = true
false  false  = false

在 Emacs 中,第二个等式的左手边显示为灰色,表示这些等式出现的顺序决定了是第一条还是第二条 会被匹配到。然而,不管是哪一条被匹配到,结果都是一样的。

相应地,给定两个可判定的命题,我们可以判定它们的析取:

infixr 5 _⊎-dec_

_⊎-dec_ :  {A B : Set}  Dec A  Dec B  Dec (A  B)
yes x ⊎-dec _     = yes (inj₁ x)
_     ⊎-dec yes y = yes (inj₂ y)
no ¬x ⊎-dec no ¬y = no λ{ (inj₁ x)  ¬x x ; (inj₂ y)  ¬y y }

两个命题的析取当任意一者成立时成立,其否定则当两者的否定成立时成立。如果任意一者成立, 我们使用其证明来作为析取的证明。如果两个的否定都成立,假设任意一者都会引入一个矛盾。

同样地,在 Emacs 中,第二条等式在左手边以灰色显示,说明等式的顺序决定了第一条还是第二条会被匹配。 这一次,我们给出的结果会因为是第二条还是第三条而不一样。如果两个命题都成立,我们选择第一个来构造析取, 但选择第二个也是同样正确的。

一个布尔值的否定当值为真时为假,反之亦然:

not : Bool  Bool
not true  = false
not false = true

相应地,给定一个可判定的命题,我们可以判定它的否定:

¬? :  {A : Set}  Dec A  Dec (¬ A)
¬? (yes x)  =  no (¬¬-intro x)
¬? (no ¬x)  =  yes ¬x

我们直接把 yes 和 no 交换。在第一个等式中,右手边断言了 ¬ A 的否定成立,也就是说 ¬ ¬ A 成立——这是一个 A 成立时可以简单得到的推论。

还有一个与蕴涵相对应,但是稍微不那么知名的运算符:

_⊃_ : Bool  Bool  Bool
_      true   =  true
false  _      =  true
true   false  =  false

当任何一个布尔值为真的时候,另一个布尔值恒为真,我们成为第一个布尔值蕴涵第二个布尔值。 因此,两者的蕴涵在第二个为真或者第一个为假时为真,在第一个为真而第二个为假时为假。 在 Emacs 中,第二个等式的左手边显示为灰色,表示这些等式出现的顺序决定了是第一条还是第二条 会被匹配到。然而,不管是哪一条被匹配到,结果都是一样的。

相应地,给定两个可判定的命题,我们可以判定它们的析取:

_→-dec_ :  {A B : Set}  Dec A  Dec B  Dec (A  B)
_     →-dec yes y  =  yes  _  y)
no ¬x →-dec _      =  yes  x  ⊥-elim (¬x x))
yes x →-dec no ¬y  =  no  f  ¬y (f x))

两者的蕴涵在第二者成立或者第一者的否定成立时成立,其否定在第一者成立而第二者否定成立时成立。 蕴涵成立的证明是一个从第一者成立的证明到第二者成立的证明的函数。如果第二者成立,那么这个函数 直接返回第二者的证明。如果第一者的否定成立,那么使用第一者成立的证明,构造一个矛盾。 如果第一者成立,第二者不成立,给定蕴涵成立的证明,我们必须构造一个矛盾:我们将成立的证明 f 应用于第一者成立的证明 x,再加以第二者否定成立的证明 ¬y 来构造矛盾。

同样地,在 Emacs 中,第二条等式在左手边以灰色显示,说明等式的顺序决定了第一条还是第二条会被匹配。 这一次,我们给出的结果会因为是哪一条被匹配而不一样,但两者都是同样正确的。

练习 erasure(实践)

证明擦除将对应的布尔值和可判定的值的操作联系了起来:

postulate
  ∧-× :  {A B : Set} (x : Dec A) (y : Dec B)   x    y    x ×-dec y 
  ∨-⊎ :  {A B : Set} (x : Dec A) (y : Dec B)   x    y    x ⊎-dec y 
  not-¬ :  {A : Set} (x : Dec A)  not  x    ¬? x 
练习 iff-erasure (推荐)

给出与同构与嵌入章节中 _↔︎_ 相对应的布尔值与可判定的值的操作, 并证明其对应的擦除:

postulate
  _iff_ : Bool  Bool  Bool
  _⇔-dec_ :  {A B : Set}  Dec A  Dec B  Dec (A  B)
  iff-⇔ :  {A B : Set} (x : Dec A) (y : Dec B)   x  iff  y    x ⇔-dec y 
-- 请将代码写在此处。

互映证明

让我们回顾一下章节自然数monus 的定义。 如果从一个较小的数中减去一个较大的数,结果为零。毕竟我们总是要得到一个结果。 我们可以用其他方式定义吗?可以定义一版带有守卫(guarded)的减法──只有当 n ≤ m 时才能从 m 中减去 n

minus : (m n : ) (n≤m : n  m)  
minus m       zero    _         = m
minus (suc m) (suc n) (s≤s n≤m) = minus m n n≤m

然而这种定义难以使用,因为我们必须显式地为 n ≤ m 提供证明:

_ : minus 5 3 (s≤s (s≤s (s≤s z≤n)))  2
_ = refl

这个问题没有通用的解决方案,但是在上述的情景下,我们恰好静态地知道这两个数字。这种情况下,我们可以使用一种被称为互映证明(proof by reflection)的技术。 实质上,在类型检查的时候我们可以让 Agda 运行可判定的等式 n ≤? m 并且保证 n ≤ m

我们使用「隐式参数」的一个特性来实现这个功能。如果 Agda 可以填充一个记录类型的所有字段,那么 Agda 就可以填充此记录类型的隐式参数。 由于空记录类型没有任何字段,Agda 总是会设法填充空记录类型的隐式参数。这就是 类型被定义成空记录的原因。

这里的技巧是设置一个类型为 T ⌊ n ≤? m ⌋ 的隐式参数。让我们一步一步阐述这句话的含义。 首先,我们运行判定过程 n ≤? m。它向我们提供了 n ≤ m 是否成立的证据。我们擦除证据得到布尔值。 最后,我们应用 T。回想一下,T 将布尔值映射到证据的世界:true 变成了单位类型 false 变成了空类型 。在操作上,这个类型的隐式参数起到了守卫的作用。

  • 如果 n ≤ m 成立,隐式参数的类型规约为 。 然后 Agda 会欣然地提供隐式参数。
  • 否则,类型规约为 ,Agda 无法为此类型提供对应的值,因此会报错。例如,如果我们调用 3 - 5 会得到 _n≤m_254 : ⊥

我们使用之前定义的 toWitness 获得了 n ≤ m 的证据:

_-_ : (m n : ) {n≤m : T  n ≤? m }  
_-_ m n {n≤m} = minus m n (toWitness n≤m)

我们现在只要能静态地知道这两个数就可以安全地使用 _-_ 了:

_ : 5 - 3  2
_ = refl

事实上,这种惯用语法非常普遍。标准库为 T ⌊ ? ⌋ 定义了叫做 True 的同义词:

True :  {Q}  Dec Q  Set
True Q = T  Q 
练习 False (实践)

给出 TruetoWitnessfromWitness相反定义。分别称为 FalsetoWitnessFalsefromWitnessFalse

练习 Bin-decidable(延伸)

回想练习 BinBin-lawsBin-predicates,定义一个用比特串表示自然数的数据类型 Bin,并定义以下谓词:

One  : Bin → Set
Can  : Bin → Set

证明以上二者是可判定的。

One? : ∀ (b : Bin) → Dec (One b)
Can? : ∀ (b : Bin) → Dec (Can b)

标准库

import Data.Bool.Base using (Bool; true; false; T; _∧_; _∨_; not)
import Data.Nat using (_≤?_)
import Relation.Nullary using (Dec; yes; no)
import Relation.Nullary.Decidable using (⌊_⌋; True; toWitness; fromWitness)
import Relation.Nullary.Negation using (¬?)
import Relation.Nullary.Product using (_×-dec_)
import Relation.Nullary.Sum using (_⊎-dec_)
import Relation.Binary using (Decidable)

Unicode

∧  U+2227  逻辑和 (\and, \wedge)
∨  U+2228  逻辑或 (\or, \vee)
⊃  U+2283  超集 (\sup)
ᵇ  U+1D47  修饰符小写 B  (\^b)
⌊  U+230A  左向下取整 (\clL)
⌋  U+230B  右向下取整 (\clR)

Lists: 列表与高阶函数

module plfa.part1.Lists where

本章节讨论列表(List)数据类型。我们用列表作为例子,来使用我们之前学习的技巧。同时, 列表也给我们带来多态类型(Polymorphic Types)和高阶函数(Higher-order Functions)的例子。

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl; sym; trans; cong)
open Eq.≡-Reasoning
open import Data.Bool using (Bool; true; false; T; _∧_; _∨_; not)
open import Data.Nat using (; zero; suc; _+_; _*_; _∸_; _≤_; s≤s; z≤n)
open import Data.Nat.Properties using
  (+-assoc; +-identityˡ; +-identityʳ; *-assoc; *-identityˡ; *-identityʳ; *-distribʳ-+)
open import Relation.Nullary using (¬_; Dec; yes; no)
open import Data.Product using (_×_; ; ∃-syntax) renaming (_,_ to ⟨_,_⟩)
open import Function using (_∘_)
open import Level using (Level)
open import plfa.part1.Isomorphism using (_≃_; _⇔_)

列表

Agda 中的列表如下定义:
data List (A : Set) : Set where
  []  : List A
  _∷_ : A  List A  List A

infixr 5 _∷_

我们来仔细研究这个定义。如果 A 是个集合,那么 List A 也是一个集合。接下来的两行告诉我们 [] (读作 nil)是一个类型为 A 的列表(通常被叫做列表),_∷_(读作 cons,是 constructor 的简写)取一个类型为 A 的值,和一个类型为 List A 的值,返回一个类型为 List A 的值。_∷_ 运算符的优先级是 5,向右结合。

例如:

_ : List 
_ = 0  1  2  []

表示了一个三个自然数的列表。因为 _∷_ 向右结合,这一项被解析成 0 ∷ (1 ∷ (2 ∷ []))。 在这里,0 是列表的第一个元素,称之为头(Head)1 ∷ (2 ∷ []) 是剩下元素的列表, 称之为尾(Tail)。列表是一个奇怪的怪兽:它有一头一尾,中间没有东西,然而它的尾巴又是一个列表!

正如我们所见,参数化的类型可以被转换成索引类型。上面的定义与下列等价:

data List′ : Set  Set where
  []′  :  {A : Set}  List′ A
  _∷′_ :  {A : Set}  A  List′ A  List′ A

每个构造子将参数作为隐式参数。因此我们列表的例子也可以写作:

_ : List 
_ = _∷_ {} 0 (_∷_ {} 1 (_∷_ {} 2 ([] {})))

此处我们将隐式参数显式地声明。

包含下面的编译器指令

{-# BUILTIN LIST List #-}

告诉 Agda,List 类型对应了 Haskell 的列表类型,构造子 []_∷_ 分别代表了 nil 和 cons,这可以让列表的表示更加的有效率。

列表语法

我们可以用下面的定义,更简便地表示列表:

pattern [_] z = z  []
pattern [_,_] y z = y  z  []
pattern [_,_,_] x y z = x  y  z  []
pattern [_,_,_,_] w x y z = w  x  y  z  []
pattern [_,_,_,_,_] v w x y z = v  w  x  y  z  []
pattern [_,_,_,_,_,_] u v w x y z = u  v  w  x  y  z  []

这是我们第一次使用模式声明。举例来说,第三行告诉我们 [ x , y , z ] 等价于 x ∷ y ∷ z ∷ []。前者可以在模式或者等式的左手边,或者是等式右手边的项中出现。

附加

我们对于列表的第一个函数写作 _++_,读作附加(Append)

infixr 5 _++_

_++_ :  {A : Set}  List A  List A  List A
[]       ++ ys  =  ys
(x  xs) ++ ys  =  x  (xs ++ ys)

A 类型是附加的隐式参数,这让这个函数变为一个多态(Polymorphic)函数 (即可以用作多种类型)。一个列表附加到空列表会得到该列表本身; 一个列表附加到非空列表所得到的列表,其头与附加到的非空列表相同,尾与所附加的列表相同。

我们举个例子,来展示将两个列表附加的计算过程:

_ : [ 0 , 1 , 2 ] ++ [ 3 , 4 ]  [ 0 , 1 , 2 , 3 , 4 ]
_ =
  begin
    0  1  2  [] ++ 3  4  []
  ≡⟨⟩
    0  (1  2  [] ++ 3  4  [])
  ≡⟨⟩
    0  1  (2  [] ++ 3  4  [])
  ≡⟨⟩
    0  1  2  ([] ++ 3  4  [])
  ≡⟨⟩
    0  1  2  3  4  []
  

附加两个列表需要对于第一个列表元素个数线性的时间。

论证附加

我们可以与用论证数几乎相同的方法来论证列表。下面是附加满足结合律的证明:
++-assoc :  {A : Set} (xs ys zs : List A)
   (xs ++ ys) ++ zs  xs ++ (ys ++ zs)
++-assoc [] ys zs =
  begin
    ([] ++ ys) ++ zs
  ≡⟨⟩
    ys ++ zs
  ≡⟨⟩
    [] ++ (ys ++ zs)
  
++-assoc (x  xs) ys zs =
  begin
    (x  xs ++ ys) ++ zs
  ≡⟨⟩
    x  (xs ++ ys) ++ zs
  ≡⟨⟩
    x  ((xs ++ ys) ++ zs)
  ≡⟨ cong (x ∷_) (++-assoc xs ys zs) 
    x  (xs ++ (ys ++ zs))
  ≡⟨⟩
    x  xs ++ (ys ++ zs)
  

证明对于第一个参数进行归纳。起始步骤将列表实例化为 [],由直接的运算可证。 归纳步骤将列表实例化为 x ∷ xs,由直接的运算配合归纳假设可证。 与往常一样,归纳假设由递归使用证明函数来表明,此处为 ++-assoc xs ys zs

回忆到 Agda 支持片段。使用 cong (x ∷_) 可以将归纳假设:

(xs ++ ys) ++ zs ≡ xs ++ (ys ++ zs)

提升至等式:

x ∷ ((xs ++ ys) ++ zs) ≡ x ∷ (xs ++ (ys ++ zs))

即证明中所需。

我们也可以简单地证明 []_++_ 的左幺元和右幺元。 左幺元的证明从定义中即可得:

++-identityˡ :  {A : Set} (xs : List A)  [] ++ xs  xs
++-identityˡ xs =
  begin
    [] ++ xs
  ≡⟨⟩
    xs
  
右幺元的证明可由简单的归纳得到:
++-identityʳ :  {A : Set} (xs : List A)  xs ++ []  xs
++-identityʳ [] =
  begin
    [] ++ []
  ≡⟨⟩
    []
  
++-identityʳ (x  xs) =
  begin
    (x  xs) ++ []
  ≡⟨⟩
    x  (xs ++ [])
  ≡⟨ cong (x ∷_) (++-identityʳ xs) 
    x  xs
  

我们之后会了解到,这三条性质表明了 _++_[] 在列表上构成了一个幺半群(Monoid)

长度

在下一个函数里,我们来寻找列表的长度:

length :  {A : Set}  List A  
length []        =  zero
length (x  xs)  =  suc (length xs)

同样,它取一个隐式参数 A。 空列表的长度为零。非空列表的长度比其尾列表长度多一。

我们用下面的例子来展示如何计算列表的长度:
_ : length [ 0 , 1 , 2 ]  3
_ =
  begin
    length (0  1  2  [])
  ≡⟨⟩
    suc (length (1  2  []))
  ≡⟨⟩
    suc (suc (length (2  [])))
  ≡⟨⟩
    suc (suc (suc (length {} [])))
  ≡⟨⟩
    suc (suc (suc zero))
  

计算列表的长度需要关于列表元素个数线性的时间。

在倒数第二行中,我们不可以直接写 length [],而需要写 length {ℕ} []。 因为 [] 没有元素,Agda 没有足够的信息来推导其隐式参数。

论证长度

两个附加在一起的列表的长度是两列表长度之和:

length-++ :  {A : Set} (xs ys : List A)
   length (xs ++ ys)  length xs + length ys
length-++ {A} [] ys =
  begin
    length ([] ++ ys)
  ≡⟨⟩
    length ys
  ≡⟨⟩
    length {A} [] + length ys
  
length-++ (x  xs) ys =
  begin
    length ((x  xs) ++ ys)
  ≡⟨⟩
    suc (length (xs ++ ys))
  ≡⟨ cong suc (length-++ xs ys) 
    suc (length xs + length ys)
  ≡⟨⟩
    length (x  xs) + length ys
  

证明对于第一个参数进行归纳。起始步骤将列表实例化为 [],由直接的运算可证。 如同之前一样,Agda 无法推导 length 的隐式参数,所以我们必须显式地给出这个参数。 归纳步骤将列表实例化为 x ∷ xs,由直接的运算配合归纳假设可证。 与往常一样,归纳假设由递归使用证明函数来表明,此处为 length-++ xs ys, 由 cong suc 来提升。

反转

我们可以使用附加,来简单地构造一个函数来反转一个列表:
reverse :  {A : Set}  List A  List A
reverse []        =  []
reverse (x  xs)  =  reverse xs ++ [ x ]

空列表的反转是空列表。 非空列表的反转是其头元素构成的单元列表附加至其尾列表反转之后的结果。

下面的例子展示了如何反转一个列表。
_ : reverse [ 0 , 1 , 2 ]  [ 2 , 1 , 0 ]
_ =
  begin
    reverse (0  1  2  [])
  ≡⟨⟩
    reverse (1  2  []) ++ [ 0 ]
  ≡⟨⟩
    (reverse (2  []) ++ [ 1 ]) ++ [ 0 ]
  ≡⟨⟩
    ((reverse [] ++ [ 2 ]) ++ [ 1 ]) ++ [ 0 ]
  ≡⟨⟩
    (([] ++ [ 2 ]) ++ [ 1 ]) ++ [ 0 ]
  ≡⟨⟩
    (([] ++ 2  []) ++ 1  []) ++ 0  []
  ≡⟨⟩
    (2  [] ++ 1  []) ++ 0  []
  ≡⟨⟩
    2  ([] ++ 1  []) ++ 0  []
  ≡⟨⟩
    (2  1  []) ++ 0  []
  ≡⟨⟩
    2  (1  [] ++ 0  [])
  ≡⟨⟩
    2  1  ([] ++ 0  [])
  ≡⟨⟩
    2  1  0  []
  ≡⟨⟩
    [ 2 , 1 , 0 ]
  

这样子反转一个列表需要列表长度二次的时间。这是因为反转一个长度为 n 的列表需要 将长度为 12 直到 n - 1 的列表附加起来,而附加两个列表需要第一个列表长度线性的时间, 因此加起来就需要 n * (n - 1) / 2 的时间。(我们将在本章节后部分验证这一结果)

练习 reverse-++-distrib(推荐)

证明一个列表附加到另外一个列表的反转即是反转后的第二个列表附加至反转后的第一个列表:

reverse (xs ++ ys) ≡ reverse ys ++ reverse xs
-- Your code goes here
练习 reverse-involutive(推荐)

当一个函数应用两次后与恒等函数作用相同,那么这个函数是一个对合(Involution)。 证明反转是一个对合:

reverse (reverse xs) ≡ xs
-- Your code goes here

更快地反转

上面的定义虽然论证起来方便,但是它比期望中的实现更低效,因为它的运行时间是关于列表长度的二次函数。 我们可以将反转进行推广,使用一个额外的参数:

shunt :  {A : Set}  List A  List A  List A
shunt []       ys  =  ys
shunt (x  xs) ys  =  shunt xs (x  ys)

这个定义对于第一个参数进行递归。第二个参数会变_大_,但这样做没有问题,因为我们递归的参数 在变_小_。

转移(Shunt)与反转的关系如下:
shunt-reverse :  {A : Set} (xs ys : List A)
   shunt xs ys  reverse xs ++ ys
shunt-reverse [] ys =
  begin
    shunt [] ys
  ≡⟨⟩
    ys
  ≡⟨⟩
    reverse [] ++ ys
  
shunt-reverse (x  xs) ys =
  begin
    shunt (x  xs) ys
  ≡⟨⟩
    shunt xs (x  ys)
  ≡⟨ shunt-reverse xs (x  ys) 
    reverse xs ++ (x  ys)
  ≡⟨⟩
    reverse xs ++ ([ x ] ++ ys)
  ≡⟨ sym (++-assoc (reverse xs) [ x ] ys) 
    (reverse xs ++ [ x ]) ++ ys
  ≡⟨⟩
    reverse (x  xs) ++ ys
  

证明对于第一个参数进行归纳。起始步骤将列表实例化为 [],由直接的运算可证。 归纳步骤将列表实例化为 x ∷ xs,由归纳假设和附加的结合律可证。 当我们使用归纳假设时,第二个参数实际上变了,但是这样做没有问题,因为我们归纳的参数变了。

使用一个会在归纳或递归的参数变小时,变大的辅助参数来进行推广,是一个常用的技巧。 这个技巧在以后的证明中很有用。

在定义了推广的转移之后,我们可以将其特化,作为一个更高效的反转的定义:

reverse′ :  {A : Set}  List A  List A
reverse′ xs = shunt xs []

因为我们之前证明的引理,我们可以直接地证明两个定义是等价的:

reverses :  {A : Set} (xs : List A)
   reverse′ xs  reverse xs
reverses xs =
  begin
    reverse′ xs
  ≡⟨⟩
    shunt xs []
  ≡⟨ shunt-reverse xs [] 
    reverse xs ++ []
  ≡⟨ ++-identityʳ (reverse xs) 
    reverse xs
  

下面的例子展示了如何快速反转列表 [ 0 , 1 , 2 ]

_ : reverse′ [ 0 , 1 , 2 ]  [ 2 , 1 , 0 ]
_ =
  begin
    reverse′ (0  1  2  [])
  ≡⟨⟩
    shunt (0  1  2  []) []
  ≡⟨⟩
    shunt (1  2  []) (0  [])
  ≡⟨⟩
    shunt (2  []) (1  0  [])
  ≡⟨⟩
    shunt [] (2  1  0  [])
  ≡⟨⟩
    2  1  0  []
  

现在反转一个列表需要的时间与列表的长度线性相关。

映射

映射将一个函数应用于列表中的所有元素,生成一个对应的列表。 映射是一个高阶函数(Higher-Order Function)的例子,它取一个函数作为参数,返回一个函数作为结果:

map :  {A B : Set}  (A  B)  List A  List B
map f []        =  []
map f (x  xs)  =  f x  map f xs

空列表的映射是空列表。 非空列表的映射生成一个列表,其头元素是原列表的头元素在应用函数之后的结果, 其尾列表是原列表的尾列表映射后的结果。

下面的例子展示了如何使用映射来增加列表中的每一个元素:

_ : map suc [ 0 , 1 , 2 ]  [ 1 , 2 , 3 ]
_ =
  begin
    map suc (0  1  2  [])
  ≡⟨⟩
    suc 0  map suc (1  2  [])
  ≡⟨⟩
    suc 0  suc 1  map suc (2  [])
  ≡⟨⟩
    suc 0  suc 1  suc 2  map suc []
  ≡⟨⟩
    suc 0  suc 1  suc 2  []
  ≡⟨⟩
    1  2  3  []
  

映射需要关于列表长度线性的时间。

我们常常可以利用柯里化,将映射作用于一个函数,获得另一个函数,然后在之后的时候应用获得的函数:

sucs : List   List 
sucs = map suc

_ : sucs [ 0 , 1 , 2 ]  [ 1 , 2 , 3 ]
_ =
  begin
    sucs [ 0 , 1 , 2 ]
  ≡⟨⟩
    map suc [ 0 , 1 , 2 ]
  ≡⟨⟩
    [ 1 , 2 , 3 ]
  

对于由另外一个类型参数化的类型,例如列表,常常有对应的映射,其接受一个函数,并返回另一个 从由给定函数定义域参数化的类型,到由给定函数值域参数化的函数。除此之外,一个对于 n 个类型 参数化的类型常常会有一个对于 n 个函数参数化的映射。

练习 map-compose(实践)

证明函数组合的映射是两个映射的组合:

map (g ∘ f) ≡ map g ∘ map f

证明的最后一步需要外延性。

-- 请将代码写在此处。
练习 map-++-distribute(实践)

证明下列关于映射与附加的关系:

map f (xs ++ ys) ≡ map f xs ++ map f ys
-- 请将代码写在此处。
练习 map-Tree(实践)

定义一个树数据类型,其叶节点类型为 A,内部节点类型为 B

data Tree (A B : Set) : Set where
  leaf : A  Tree A B
  node : Tree A B  B  Tree A B  Tree A B

定义一个对于树的映射运算符:

map-Tree : ∀ {A B C D : Set} → (A → C) → (B → D) → Tree A B → Tree C D
-- 请将代码写在此处。

折叠

折叠取一个运算符和一个值,并使用运算符将列表中的元素合并至一个值,如果给定的列表为空, 则使用给定的值:

foldr :  {A B : Set}  (A  B  B)  B  List A  B
foldr _⊗_ e []        =  e
foldr _⊗_ e (x  xs)  =  x  foldr _⊗_ e xs

空列表的折叠是给定的值。 非空列表的折叠使用给定的运算符,将头元素和尾列表的折叠合并起来。

下面的例子展示了如何使用折叠来对一个列表求和:

_ : foldr _+_ 0 [ 1 , 2 , 3 , 4 ]  10
_ =
  begin
    foldr _+_ 0 (1  2  3  4  [])
  ≡⟨⟩
    1 + foldr _+_ 0 (2  3  4  [])
  ≡⟨⟩
    1 + (2 + foldr _+_ 0 (3  4  []))
  ≡⟨⟩
    1 + (2 + (3 + foldr _+_ 0 (4  [])))
  ≡⟨⟩
    1 + (2 + (3 + (4 + foldr _+_ 0 [])))
  ≡⟨⟩
    1 + (2 + (3 + (4 + 0)))
  

折叠需要关于列表长度线性的时间。

我们常常可以利用柯里化,将折叠作用于一个运算符和一个值,获得另一个函数, 然后在之后的时候应用获得的函数:

sum : List   
sum = foldr _+_ 0

_ : sum [ 1 , 2 , 3 , 4 ]  10
_ =
  begin
    sum [ 1 , 2 , 3 , 4 ]
  ≡⟨⟩
    foldr _+_ 0 [ 1 , 2 , 3 , 4 ]
  ≡⟨⟩
    10
  

正如列表由两个构造子 []_∷_,折叠函数取两个参数 e_⊗_ (除去列表参数)。推广来说,一个有 n 个构造子的数据类型,会有对应的 取 n 个参数的折叠函数。

举另外一个例子,观察

foldr _∷_ [] xs ≡ xs

xs 的类型为 List A,那么我们就会有一个 foldr 的实例,其中的 AA,而 BList A。它遵循

xs ++ ys ≡ foldr _∷_ ys xs

二者相等的证明留作练习。

练习 product (推荐)

使用折叠来定义一个计算列表数字之积的函数。例如:

product [ 1 , 2 , 3 , 4 ] ≡ 24
-- 请将代码写在此处。
练习 foldr-++ (推荐)

证明折叠和附加有如下的关系:

foldr _⊗_ e (xs ++ ys) ≡ foldr _⊗_ (foldr _⊗_ e ys) xs
-- 请将代码写在此处。
Exercise foldr-∷ (practice)

Show

foldr _∷_ [] xs ≡ xs

Show as a consequence of foldr-++ above that

xs ++ ys ≡ foldr _∷_ ys xs
练习 map-is-foldr

证明映射可以用折叠定义:

-- 请将代码写在此处。

此证明需要外延性。

练习 map-is-foldr(实践)

请证明 map 可使用 fold 来定义:

map f ≡ foldr (λ x xs → f x ∷ xs) []

此证明需要外延性。

-- 请将代码写在此处。
练习 fold-Tree(实践)

请为预先给定的三个类型定义一个合适的折叠函数:

fold-Tree : ∀ {A B C : Set} → (A → C) → (C → B → C → C) → Tree A B → C
-- 请将代码写在此处。
练习 map-is-fold-Tree(实践)

对于树数据类型,证明与 map-is-foldr 相似的性质。

-- 请将代码写在此处。
证明 sum-downFrom (延伸)

定义一个向下数数的函数:

downFrom :   List 
downFrom zero     =  []
downFrom (suc n)  =  n  downFrom n

例如:

_ : downFrom 3  [ 2 , 1 , 0 ]
_ = refl

证明数列之和 (n - 1) + ⋯ + 0 等于 n * (n ∸ 1) / 2

sum (downFrom n) * 2 ≡ n * (n ∸ 1)
-- 请将代码写在此处。

幺半群

一般来说,我们会对于折叠函数使用一个满足结合律的运算符,和这个运算符的左右幺元。 这意味着这个运算符和这个值形成了一个幺半群

我们可以用一个合适的记录类型来定义幺半群:

record IsMonoid {A : Set} (_⊗_ : A  A  A) (e : A) : Set where
  field
    assoc :  (x y z : A)  (x  y)  z  x  (y  z)
    identityˡ :  (x : A)  e  x  x
    identityʳ :  (x : A)  x  e  x

open IsMonoid

举例来说,加法和零,乘法和一,附加和空列表,都是幺半群:

+-monoid : IsMonoid _+_ 0
+-monoid =
  record
    { assoc = +-assoc
    ; identityˡ = +-identityˡ
    ; identityʳ = +-identityʳ
    }

*-monoid : IsMonoid _*_ 1
*-monoid =
  record
    { assoc = *-assoc
    ; identityˡ = *-identityˡ
    ; identityʳ = *-identityʳ
    }

++-monoid :  {A : Set}  IsMonoid {List A} _++_ []
++-monoid =
  record
    { assoc = ++-assoc
    ; identityˡ = ++-identityˡ
    ; identityʳ = ++-identityʳ
    }

如果 _⊗_e 构成一个幺半群,那么我们可以用相同的运算符和一个任意的值来表示折叠:

foldr-monoid :  {A : Set} (_⊗_ : A  A  A) (e : A)  IsMonoid _⊗_ e 
   (xs : List A) (y : A)  foldr _⊗_ y xs  foldr _⊗_ e xs  y
foldr-monoid _⊗_ e ⊗-monoid [] y =
  begin
    foldr _⊗_ y []
  ≡⟨⟩
    y
  ≡⟨ sym (identityˡ ⊗-monoid y) 
    (e  y)
  ≡⟨⟩
    foldr _⊗_ e []  y
  
foldr-monoid _⊗_ e ⊗-monoid (x  xs) y =
  begin
    foldr _⊗_ y (x  xs)
  ≡⟨⟩
    x  (foldr _⊗_ y xs)
  ≡⟨ cong (x ⊗_) (foldr-monoid _⊗_ e ⊗-monoid xs y) 
    x  (foldr _⊗_ e xs  y)
  ≡⟨ sym (assoc ⊗-monoid x (foldr _⊗_ e xs) y) 
    (x  foldr _⊗_ e xs)  y
  ≡⟨⟩
    foldr _⊗_ e (x  xs)  y
  

在之前的练习中,我们证明了以下定理:

postulate
  foldr-++ :  {A : Set} (_⊗_ : A  A  A) (e : A) (xs ys : List A) 
    foldr _⊗_ e (xs ++ ys)  foldr _⊗_ (foldr _⊗_ e ys) xs

由此,我们可以将幺半群中附加的折叠如下分解成两个折叠:

foldr-monoid-++ :  {A : Set} (_⊗_ : A  A  A) (e : A)  IsMonoid _⊗_ e 
   (xs ys : List A)  foldr _⊗_ e (xs ++ ys)  foldr _⊗_ e xs  foldr _⊗_ e ys
foldr-monoid-++ _⊗_ e monoid-⊗ xs ys =
  begin
    foldr _⊗_ e (xs ++ ys)
  ≡⟨ foldr-++ _⊗_ e xs ys 
    foldr _⊗_ (foldr _⊗_ e ys) xs
  ≡⟨ foldr-monoid _⊗_ e monoid-⊗ xs (foldr _⊗_ e ys) 
    foldr _⊗_ e xs  foldr _⊗_ e ys
  
练习 foldl(实践)

定义一个函数 foldl,与 foldr 相似,但是运算符向左结合,而不是向右。例如:

foldr _⊗_ e [ x , y , z ]  =  x ⊗ (y ⊗ (z ⊗ e))
foldl _⊗_ e [ x , y , z ]  =  ((e ⊗ x) ⊗ y) ⊗ z
-- 请将代码写在此处。
练习 foldr-monoid-foldl(实践)

证明如果 _⊗_e 构成幺半群,那么 foldr _⊗_ efoldl _⊗_ e 的结果 永远是相同的。

-- 请将代码写在此处。

所有

我们也可以定义关于列表的谓词。最重要的两个谓词是 AllAny

谓词 All P 当列表里的所有元素满足 P 时成立:

data All {A : Set} (P : A  Set) : List A  Set where
  []  : All P []
  _∷_ :  {x : A} {xs : List A}  P x  All P xs  All P (x  xs)

这个类型有两个构造子,使用了与列表构造子相同的名称。第一个断言了 P 对于空列表的任何元素成立。 第二个断言了如果 P 对于列表的头元素和尾列表的所有元素成立,那么 P 对于这个列表的任何元素成立。 Agda 使用类型来区分构造子是用于构造一个列表,还是构造 All P 成立的证明。

比如说,All (_≤ 2) 对于一个每一个元素都小于等于二的列表成立。 回忆 z≤n 证明了对于任意 nzero ≤ n 成立; 对于任意 mn,如果 m≤n 证明了 m ≤ n,那么 s≤s m≤n 证明了 suc m ≤ suc n:

_ : All (_≤ 2) [ 0 , 1 , 2 ]
_ = z≤n  s≤s z≤n  s≤s (s≤s z≤n)  []

这里 _∷_[]All P 的构造子,而不是 List A 的。 这三项分别是 0 ≤ 21 ≤ 22 ≤ 2 的证明。

(读者可能会思考诸如 [_,_,_] 的模式是否可以用于构造 All 类型的值, 像构造 List 类型的一样,因为两者使用了相同的构造子。事实上这样做是可以的,只要两个类型 在模式声明时在作用域内。然而现在不是这样的情况,因为 List 先于 [_,_,_] 定义,而 All 在 之后定义。)

任意

谓词 Any P 当列表里的一些元素满足 P 时成立:

data Any {A : Set} (P : A  Set) : List A  Set where
  here  :  {x : A} {xs : List A}  P x  Any P (x  xs)
  there :  {x : A} {xs : List A}  Any P xs  Any P (x  xs)

第一个构造子证明了列表的头元素满足 P,第二个构造子证明的列表的尾列表中的一些元素满足 P。 举例来说,我们可以如下定义列表的成员关系:

infix 4 _∈_ _∉_

_∈_ :  {A : Set} (x : A) (xs : List A)  Set
x  xs = Any (x ≡_) xs

_∉_ :  {A : Set} (x : A) (xs : List A)  Set
x  xs = ¬ (x  xs)

比如说,零是列表 [ 0 , 1 , 0 , 2 ] 中的一个元素。 我们可以用两种方法来展示这个事实,对应零在列表中出现了两次:第一个元素和第三个元素:

_ : 0  [ 0 , 1 , 0 , 2 ]
_ = here refl

_ : 0  [ 0 , 1 , 0 , 2 ]
_ = there (there (here refl))

除此之外,我们可以展示三不在列表之中,因为任何它在列表中的证明会推导出矛盾:

not-in : 3  [ 0 , 1 , 0 , 2 ]
not-in (here ())
not-in (there (here ()))
not-in (there (there (here ())))
not-in (there (there (there (here ()))))
not-in (there (there (there (there ()))))

() 出现了五次,分别表示没有 3 ≡ 03 ≡ 13 ≡ 03 ≡ 23 ∈ [] 的证明。

所有和附加

一个谓词对两个附加在一起的列表的每个元素都成立,当且仅当这个谓词对两个列表的每个元素都成立:

All-++-⇔ :  {A : Set} {P : A  Set} (xs ys : List A) 
  All P (xs ++ ys)  (All P xs × All P ys)
All-++-⇔ xs ys =
  record
    { to       =  to xs ys
    ; from     =  from xs ys
    }
  where

  to :  {A : Set} {P : A  Set} (xs ys : List A) 
    All P (xs ++ ys)  (All P xs × All P ys)
  to [] ys Pys =  [] , Pys 
  to (x  xs) ys (Px  Pxs++ys) with to xs ys Pxs++ys
  ... |  Pxs , Pys  =  Px  Pxs , Pys 

  from :  { A : Set} {P : A  Set} (xs ys : List A) 
    All P xs × All P ys  All P (xs ++ ys)
  from [] ys  [] , Pys  = Pys
  from (x  xs) ys  Px  Pxs , Pys  =  Px  from xs ys  Pxs , Pys 
练习 Any-++-⇔ (推荐)

使用 Any 代替 All 与一个合适的 _×_ 的替代,证明一个类似于 All-++-⇔ 的结果。 作为结论,展示关联 _∈__++_ 的一个等价关系。

-- 请将代码写在此处。
练习 All-++-≃ (延伸)

证明 All-++-⇔ 的等价关系可以被扩展至一个同构关系。

-- 请将代码写在此处。
练习 ¬Any⇔All¬(推荐)

请证明 AnyAll 满足一个版本的德摩根定律:

(¬_ ∘ Any P) xs ⇔ All (¬_ ∘ P) xs

(你能明白为什么这里的 _∘_ 被泛化到任意层级很重要吗? 如全体多态一节所述。)

以下定律是否也成立?

(¬_ ∘ All P) xs ⇔ Any (¬_ ∘ P) xs

若成立,请证明;否则请解释原因。

-- 请将代码写在此处。
练习 ¬Any≃All¬(拓展)

请证明等价的 ¬Any⇔All¬ 可以被扩展成一个同构。 你需要使用外延性。

-- 请将代码写在此处
练习 All-∀(实践)

请证明 All P xs 同构于 ∀ x → x ∈ xs → P x.

-- 请将代码写在此处
练习 Any-∃(实践)

请证明 Any P xs 同构于 ∃[ x ] (x ∈ xs × P x).

-- 请将代码写在此处

如果成立,请证明;如果不成立,请解释原因。

所有的可判定性

如果我们将一个谓词看作一个返回布尔值的函数,那么我们可以简单的定义一个类似于 All 的函数,其当给定谓词对于列表每个元素返回真时返回真:

all :  {A : Set}  (A  Bool)  List A  Bool
all p  =  foldr _∧_ true  map p

我们可以使用高阶函数 mapfoldr 来简洁地写出这个函数。

正如所希望的那样,如果我们将布尔值替换成可判定值,这与 All 是相似的。首先,回到将 P 当作一个类型为 A → Set 的函数的概念,将一个类型为 A 的值 x 转换成 P xx 成立 的证明。我们成 P可判定的(Decidable),如果我们有一个函数,其在给定 x 时能够判定 P x

Decidable :  {A : Set}  (A  Set)  Set
Decidable {A} P  =   (x : A)  Dec (P x)
那么当谓词 P 可判定时,我们亦可判定列表中的每一个元素是否满足这个谓词:
All? :  {A : Set} {P : A  Set}  Decidable P  Decidable (All P)
All? P? []                                 =  yes []
All? P? (x  xs) with P? x   | All? P? xs
...                 | yes Px | yes Pxs     =  yes (Px  Pxs)
...                 | no ¬Px | _           =  no λ{ (Px  Pxs)  ¬Px Px   }
...                 | _      | no ¬Pxs     =  no λ{ (Px  Pxs)  ¬Pxs Pxs }

如果列表为空,那么 P 显然对列表的每个元素成立。 否则,证明的结构与两个可判定的命题是可判定的证明相似,不过我们使用 _∷_ 而不是 ⟨_,_⟩ 来整合头元素和尾列表的证明。

练习 Any?(延伸)

正如 All 有类似的 allAll? 形式,来判断列表的每个元素是否满足给定的谓词, 那么 Any 也有类似的 anyAny? 形式,来判断列表的一些元素是否满足给定的谓词。 给出它们的定义。

-- 请将代码写在此处。
练习 split(延伸)

关系 merge 在两个列表合并的结果为给定的第三个列表时成立。

data merge {A : Set} : (xs ys zs : List A)  Set where

  [] :
      --------------
      merge [] [] []

  left-∷ :  {x xs ys zs}
     merge xs ys zs
      --------------------------
     merge (x  xs) ys (x  zs)

  right-∷ :  {y xs ys zs}
     merge xs ys zs
      --------------------------
     merge xs (y  ys) (y  zs)

例如

_ : merge [ 1 , 4 ] [ 2 , 3 ] [ 1 , 2 , 3 , 4 ]
_ = left-∷ (right-∷ (right-∷ (left-∷ [])))

给定一个可判定谓词和一个列表,我们可以将该列表拆分成两个列表, 二者可以合并成原列表,其中一个列表的所有元素都满足该谓词, 而另一个列表中的所有元素都不满足该谓词。

在列表上定义一个传统 filter 函数的变体,如下所示,它接受一个可判定谓词 和一个列表,返回一个所有元素都满足该谓词的列表,和一个所有元素都不满足的列表, 以及与它们相应的证明。

split : ∀ {A : Set} {P : A → Set} (P? : Decidable P) (zs : List A)
  → ∃[ xs ] ∃[ ys ] ( merge xs ys zs × All P xs × All (¬_ ∘ P) ys )
-- 请将代码写在此处

标准库

标准库中可以找到与本章节中相似的定义:

import Data.List using (List; _++_; length; reverse; map; foldr; downFrom)
import Data.List.Relation.Unary.All using (All; []; _∷_)
import Data.List.Relation.Unary.Any using (Any; here; there)
import Data.List.Membership.Propositional using (_∈_)
import Data.List.Properties
  using (reverse-++-commute; map-compose; map-++-commute; foldr-++)
  renaming (mapIsFold to map-is-foldr)
import Algebra.Structures using (IsMonoid)
import Relation.Unary using (Decidable)
import Relation.Binary using (Decidable)

标准库中的 IsMonoid 与给出的定义不同,因为它可以针对特定的等价关系参数化。

Relation.UnaryRelation.Binary 都定义了 Decidable 的某个版本,一个 用于单元关系(正如本章中的单元谓词 P),一个用于二元关系(正如之前使用的 _≤_)。

Unicode

本章使用了下列 Unicode:

∷  U+2237  比例  (\::)
⊗  U+2297  带圈的乘号  (\otimes, \ox)
∈  U+2208  元素属于  (\in)
∉  U+2209  元素不属于  (\inn, \notin)

第二分册:编程语言基础

Lambda: λ-演算简介

module plfa.part2.Lambda where

λ-演算,最早由逻辑学家 Alonzo Church 在 1932 年发表,是一种只含有三种构造的演算—— 变量(Variable)、抽象(Abstraction)与应用(Application)。 λ-演算刻画了函数抽象(Functional Abstraction)的核心概念。这样的概念 以函数、过程和方法的形式,在基本上每一个编程语言中都有体现。 简单类型的 λ-演算(Simply-Typed Lambda Calculus,简写为 STLC)是 λ-演算的一种变体, 由 Church 在 1940 年发表。 除去之前提到的三种构造,简单类型的 λ-演算还拥有函数类型和任何所需的基本类型。 Church 使用了最简单的没有任何操作的基本类型。 我们在这里使用 Plotkin 的可编程的可计算函数(Programmable Computable Functions,PCF), 并加入自然数和递归函数及其相关操作。

在本章中,我们将形式化简单类型的 λ-演算,给出它的语法、小步语义和类型规则。 在下一章 Properties 中,我们将 证明它的主要性质,包括可进性与保型性。 后续的章节将研究 λ-演算的不同变体。

请注意,我们在这里使用的方法不是将它形式化的推荐方法。使用 de Bruijn 索引和 固有类型的项(我们会在 DeBruijn 章节中进一步研究), 可以让我们的形式化更简洁。 不过,我们先从使用带名字的变量和外在类型的项来表示 λ-演算开始。 一方面是因为名字比索引更易于阅读,另一方面是因为这样的表述更加传统。

这一章启发自《软件基础》(Software Foundations)/《程序语言基础》(Programming Language Foundations)中对应的 Stlc 的内容。 我们的不同之处在于使用显式的方法来表示语境(由标识符和类型的序对组成的列表), 而不是部分映射(从标识符到类型的部分函数)。 这样的做法与后续的 de Bruijn 索引表示方法能更好的对应。 我们使用自然数作为基础类型,而不是布尔值,这样我们可以表示更复杂的例子。 特别的是,我们将可以证明(两次!)二加二得四。

导入

open import Data.Bool using (Bool; true; false; T; not)
open import Data.Empty using (; ⊥-elim)
open import Data.List using (List; _∷_; [])
open import Data.Nat using (; zero; suc)
open import Data.Product using (∃-syntax; _×_)
open import Data.String using (String; _≟_)
open import Data.Unit using (tt)
open import Relation.Nullary using (Dec; yes; no; ¬_)
open import Relation.Nullary.Decidable using (False; toWitnessFalse)
open import Relation.Nullary.Negation using (¬?)
open import Relation.Binary.PropositionalEquality using (_≡_; _≢_; refl)

项的语法

项由七种构造组成。首先是 λ-演算中核心的三个构造:

  • 变量 ` x
  • 抽象 ƛ x ⇒ N
  • 应用 L · M

三个与自然数有关的构造:

  • `zero
  • 后继 `suc M
  • 匹配 case L [zero⇒ M |suc x ⇒ N ]

一个与递归有关的构造:

  • 不动点 μ x ⇒ M

抽象也被叫做 λ-抽象,这也是 λ-演算名字的由来。

除了变量和不动点以外,每一个项要么构造了一个给定类型的值 (抽象产生了函数,零和后继产生了自然数), 要么解构了一个这样的值(应用使用了函数,匹配使用了自然数)。 我们在给项赋予类型的时候将重新探讨这一对应关系。 构造子对应了引入规则,解构子对应了消去规则。

下面是以 Backus-Naur 范式(BNF)给出的语法:

L, M, N  ::=
  ` x  |  ƛ x ⇒ N  |  L · M  |
  `zero  |  `suc M  |  case L [zero⇒ M |suc x ⇒ N ]  |
  μ x ⇒ M

而下面是用 Agda 形式化后的代码:

Id : Set
Id = String

infix  5  ƛ_⇒_
infix  5  μ_⇒_
infixl 7  _·_
infix  8  `suc_
infix  9  `_

data Term : Set where
  `_                      :  Id  Term
  ƛ_⇒_                    :  Id  Term  Term
  _·_                     :  Term  Term  Term
  `zero                   :  Term
  `suc_                   :  Term  Term
  case_[zero⇒_|suc_⇒_]    :  Term  Term  Id  Term  Term
  μ_⇒_                    :  Id  Term  Term

我们用字符串来表示标识符。 我们使用的优先级使得 λ-抽象和不动点结合的最不紧密,其次是应用,再是后继, 结合得最紧密的是变量的构造子。 匹配表达式自带了括号。

项的例子

下面是一些项的例子:自然数二和一个将自然数相加的函数:

two : Term
two = `suc `suc `zero

plus : Term
plus = μ "+"  ƛ "m"  ƛ "n" 
         case ` "m"
           [zero⇒ ` "n"
           |suc "m"  `suc (` "+" · ` "m" · ` "n") ]

加法的递归定义与我们一开始在 Naturals 章节中定义的 _+_ 相似。 在这里,变量「m」被约束了两次,一个在 λ-抽象中,另一次在匹配表达式的后继分支中。 第一次使用的「m」指代前者,第二次使用的指代后者。 任何在后继分支中的「m」必须指代后者,因此我们称之为后者屏蔽(Shadow)了前者。 后面我们会证实二加二得四,也就是说下面的项

plus · two · two

会规约为 `suc `suc `suc `suc `zero

第二个例子里,我们使用高阶函数来表示自然数。 具体来说,数字 n 由一个接受两个参数的函数来表示,这个函数将第一个参数 应用于第二个参数上 n 次。 这样的表示方法叫做自然数的 Church 表示法。 下面是一些项的例子:Church 表示法的数字二、一个将两个用 Church 表示法表示的数字相加的函数和 一个计算后继的函数:

twoᶜ : Term
twoᶜ =  ƛ "s"  ƛ "z"  ` "s" · (` "s" · ` "z")

plusᶜ : Term
plusᶜ =  ƛ "m"  ƛ "n"  ƛ "s"  ƛ "z" 
         ` "m" · ` "s" · (` "n" · ` "s" · ` "z")

sucᶜ : Term
sucᶜ = ƛ "n"  `suc (` "n")

Church 法表示的二取两个参数 sz,将 s 应用于 z 两次。 加法取两个数 mn,函数 s 和参数 z,使用 ms 应用于 使用 n 应用于 sz 的结果。因此 s 对于 z 被应用了 mn 次。 为了方便起见,我们定义一个计算后继的函数。 为了将一个 Church 数转化为对应的自然数,我们将它应用到 sucᶜ 函数和自然数零上。 同样,我们之后会证明二加二得四,也就是说,下面的项

plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero

会规约为 `suc `suc `suc `suc `zero

练习 mul (推荐)

写出一个项来定义两个自然数的乘法。你可以使用之前定义的 plus

-- 请将代码写在此处。
练习 mulᶜ (习题)

写出一个项来定义两个用 Church 法表示的自然数的乘法。 你可以使用之前定义的 plusᶜ。 (当然也可以不用,用或不用都有很好的表示方法)

-- 请将代码写在此处。
练习 primed (延伸)

` "x" 而不是 x 来表示变量可能并不是每个人都喜欢。 我们可以加入下面的定义,来帮助我们表示项的例子:

var? : (t : Term)  Bool
var? (` _)  =  true
var? _      =  false

ƛ′_⇒_ : (t : Term)  {_ : T (var? t)}  Term  Term
ƛ′_⇒_ (` x) N = ƛ x  N

case′_[zero⇒_|suc_⇒_] : Term  Term  (t : Term)  {_ : T (var? t)}  Term  Term
case′ L [zero⇒ M |suc (` x)  N ]  =  case L [zero⇒ M |suc x  N ]

μ′_⇒_ : (t : Term)  {_ : T (var? t)}  Term  Term
μ′ (` x)  N  =  μ x  N

回顾 T 是一个将计算映射到证明的函数,如同在 Decidable 章节中的定义。 我们只在项参数是变量时使用带一撇的函数,用隐式证明来保证这一点。 例如,如果我们试着定义一个绑定非变量的抽象:

_ : Term
_ = ƛ′ two ⇒ two

Agda 会抱怨它无法为隐式参数找到一个底类型的值。 注意此处隐式参数的类型在 t 不是变量时会规约至

现在我们可以用下面的形式重新写出 plus 的定义:

plus′ : Term
plus′ = μ′ +  ƛ′ m  ƛ′ n 
          case′ m
            [zero⇒ n
            |suc m  `suc (+ · m · n) ]
  where
  +  =  ` "+"
  m  =  ` "m"
  n  =  ` "n"

用这样的形式写出乘法的定义。

形式化与非正式

在形式化语义的非正式表述中,我们使用变量名来消除歧义,用 x 而不是 ` x 来表示一个变量项。Agda 要求我们对两者进行区分。

相似地来说,非正式的表达通常在对象语言(Object Language)(我们正在描述的语言) 和元语言(Meta-Language)(我们用来描述对象语言的语言) 中使用相同的记法来表示函数类型、λ-抽象和函数应用,相信读者可以通过语境区分两种语言。 而 Agda 并不能做到这样,因此我们在对象语言中使用 ƛ x ⇒ NL · M , 与我们使用的元语言 Agda 中的 λ x → NL M 相对。

约束变量与自由变量

在抽象 ƛ x ⇒ N 中,我们把 x 叫做约束变量N 叫做抽象体。 λ-演算一个重要的特性是将相同的约束变量同时重命名不会改变一个项的意义。 因此下面的五个项

  • ƛ "s" ⇒ ƛ "z" ⇒ ` "s" · (` "s" · ` "z")
  • ƛ "f" ⇒ ƛ "x" ⇒ ` "f" · (` "f" · ` "x")
  • ƛ "sam" ⇒ ƛ "zelda" ⇒ ` "sam" · (` "sam" · ` "zelda")
  • ƛ "z" ⇒ ƛ "s" ⇒ ` "z" · (` "z" · ` "s")
  • ƛ "😇" ⇒ ƛ "😈" ⇒ ` "😇" · (` "😇" · ` "😈" )

都可以认为是等价的。使用 Haskell Curry 引入的约定,这样的规则 用希腊字母 αalpha) 来表示,因此这样的等价关系也叫做 α-重命名

当我们从一个项中观察它的子项时,被约束的变量可能会变成自由变量。 考虑下面的项:

  • ƛ "s" ⇒ ƛ "z" ⇒ ` "s" · (` "s" · ` "z") sz 都是约束变量。

  • ƛ "z" ⇒ ` "s" · (` "s" · ` "z") z 是约束变量,s 是自由变量。

  • ` "s" · (` "s" · ` "z") sz 都是自由变量。

我们将没有自由变量的项叫做闭项,否则它是一个开项。 上面的三个项中,第一个是闭项,剩下两个是开项。我们在讨论规约时,会注重闭项。

一个变量在不同地方出现时,可以同时是约束变量和自由变量。在下面的项中:

(ƛ "x" ⇒ ` "x") · ` "x"

内部的 x 是约束变量,外部的是自由变量。使用 α-重命名,上面的项等同于

(ƛ "y" ⇒ ` "y") · ` "x"

此处 y 是约束变量,x 是自由变量。Barendregt 约定,一个常见的约定,使用 α-重命名 来保证约束变量与自由变量完全不同。这样可以避免因为约束变量和自由变量名称相同而造成的混乱。

匹配和递归同样引入了约束变量,我们也可以使用 α-重命名。下面的项

μ "+" ⇒ ƛ "m" ⇒ ƛ "n" ⇒
  case ` "m"
    [zero⇒ ` "n"
    |suc "m" ⇒ `suc (` "+" · ` "m" · ` "n") ]

注意这个项包括了两个 m 的不同绑定,第一次出现在第一行,第二次出现在最后一行。 这个项与下面的项等同

μ "plus" ⇒ ƛ "x" ⇒ ƛ "y" ⇒
  case ` "x"
    [zero⇒ ` "y"
    |suc "x′" ⇒ `suc (` "plus" · ` "x′" · ` "y") ]

其中两次出现的 m 现在用 xx′ 两个不同的名字表示。

值(Value)是一个对应着答案的项。 因此,`suc `suc `suc `suc `zero 是一个值, 而 plus · two · two 不是。 根据惯例,我们将所有的抽象当作值;所以 plus本身是一个值。

谓词 Value M 当一个项 M 是一个值时成立:

data Value : Term  Set where

  V-ƛ :  {x N}
      ---------------
     Value (ƛ x  N)

  V-zero :
      -----------
      Value `zero

  V-suc :  {V}
     Value V
      --------------
     Value (`suc V)

后文中我们用 VW 来表示值。

正式与非正式

在形式化语义的非正式表达中,我们用元变量 V 来表示一个值。 在 Agda 中,我们必须使用 Value 谓词来显式地表达。

其他方法

另一种定义不注重封闭的项,将变量视作值。 ƛ x ⇒ N 只有在 N 是一个值的时候,才是一个值。 这是 Agda 范式化项的方法,我们在 Untyped 章节中考虑这种方法。

替换

λ-演算的核心操作是将一个项中的变量用另一个项来替换。 替换在定义函数应用的操作语义中起到了重要的作用。 比如说,我们有

  (ƛ "s" ⇒ ƛ "z" ⇒ ` "s" · (` "s" · ` "z")) · sucᶜ · `zero
—→
  (ƛ "z" ⇒ sucᶜ · (sucᶜ · ` "z")) · `zero
—→
  sucᶜ · (sucᶜ · `zero)

其中,我们在抽象体中用 sucᶜ 替换 ` "s",用 `zero 替换 ` "z"

我们将替换写作 N [ x := V ],意为用 V 来替换项 N 中出现的所有自由变量 x。 简短地说,就是用 V 来替换 N 中的 x,或者是把 N 中的 x 换成 V。 替换只在 V 是一个封闭项时有效。它不一定是一个值,我们在这里使用 V 是因为 常常我们使用值进行替换。

下面是一些例子:

  • (ƛ "z" ⇒ ` "s" · (` "s" · ` "z")) [ "s" := sucᶜ ]ƛ "z" ⇒ sucᶜ · (sucᶜ · ` "z")
  • (sucᶜ · (sucᶜ · ` "z")) [ "z" := `zero ]sucᶜ · (sucᶜ · `zero)
  • (ƛ "x" ⇒ ` "y") [ "y" := `zero ]ƛ "x" ⇒ `zero
  • (ƛ "x" ⇒ ` "x") [ "x" := `zero ]ƛ "x" ⇒ ` "x"
  • (ƛ "y" ⇒ ` "y") [ "x" := `zero ]ƛ "y" ⇒ ` "y"

在倒数第二个例子中,用 `zeroƛ "x" ⇒ ` "x" 出现的 x 得到的不是 ƛ "x" ⇒ `zero, 因为 x 是抽象中的约束变量。 约束变量的名称是无关的, ƛ "x" ⇒ ` "x"ƛ "y" ⇒ ` "y" 都是恒等函数。 可以认为 x 在抽象体内和抽象体外是不同的变量,而它们恰好拥有一样的名字。

我们将要给出替换的定义在用来替换变量的项是封闭时有效。 这是因为用封闭的项可能需要对于约束变量进行重命名。例如:

  • (ƛ "x" ⇒ ` "x" · ` "y") [ "y" := ` "x" · `zero] 不应该得到
    (ƛ "x" ⇒ ` "x" · (` "x" · `zero)).

不同如上,我们应该将约束变量进行重命名,来防止捕获:

  • (ƛ "x" ⇒ ` "x" · ` "y") [ "y" := ` "x" · `zero ] 应该得到
    ƛ "x′" ⇒ ` "x′" · (` "x" · `zero).

这里的 x′ 是一个新的、不同于 x 的变量。 带有重命名的替换的形式化定义更加复杂。在这里,我们将替换限制在封闭的项之内, 可以避免重命名的问题,但对于我们要做的后续的内容来说也是足够的。

下面是对于封闭项替换的 Agda 定义:

infix 9 _[_:=_]

_[_:=_] : Term  Id  Term  Term
(` x) [ y := V ] with x  y
... | yes _         = V
... | no  _         = ` x
(ƛ x  N) [ y := V ] with x  y
... | yes _         = ƛ x  N
... | no  _         = ƛ x  N [ y := V ]
(L · M) [ y := V ]  = L [ y := V ] · M [ y := V ]
(`zero) [ y := V ]  = `zero
(`suc M) [ y := V ] = `suc M [ y := V ]
(case L [zero⇒ M |suc x  N ]) [ y := V ] with x  y
... | yes _         = case L [ y := V ] [zero⇒ M [ y := V ] |suc x  N ]
... | no  _         = case L [ y := V ] [zero⇒ M [ y := V ] |suc x  N [ y := V ] ]
(μ x  N) [ y := V ] with x  y
... | yes _         = μ x  N
... | no  _         = μ x  N [ y := V ]

下面我们来看一看前三个情况:

  • 对于变量,我们将需要替换的变量 y 与项中的变量 x 进行比较。 如果它们相同,我们返回 V,否则返回 x 不变。
  • 对于抽象,我们将需要替换的变量 y 与抽象中的约束变量 x 进行比较。 如果它们相同,我们返回抽象不变,否则对于抽象体内部进行替换。
  • 对于应用,我们递归地替换函数和其参数。

匹配表达式和递归也有约束变量,我们使用与抽象相似的方法处理它们。 除此之外的情况,我们递归地对于子项进行替换。

例子

下面是上述替换正确性的证明:

_ : (ƛ "z"  ` "s" · (` "s" · ` "z")) [ "s" := sucᶜ ]
       ƛ "z"  sucᶜ · (sucᶜ · ` "z")
_ = refl

_ : (sucᶜ · (sucᶜ · ` "z")) [ "z" := `zero ]  sucᶜ · (sucᶜ · `zero)
_ = refl

_ : (ƛ "x"  ` "y") [ "y" := `zero ]  ƛ "x"  `zero
_ = refl

_ : (ƛ "x"  ` "x") [ "x" := `zero ]  ƛ "x"  ` "x"
_ = refl

_ : (ƛ "y"  ` "y") [ "x" := `zero ]  ƛ "y"  ` "y"
_ = refl
小测验

下面替换的结果是?

(ƛ "y" ⇒ ` "x" · (ƛ "x" ⇒ ` "x")) [ "x" := `zero ]
  1. (ƛ "y" ⇒ ` "x" · (ƛ "x" ⇒ ` "x"))
  2. (ƛ "y" ⇒ ` "x" · (ƛ "x" ⇒ `zero))
  3. (ƛ "y" ⇒ `zero · (ƛ "x" ⇒ ` "x"))
  4. (ƛ "y" ⇒ `zero · (ƛ "x" ⇒ `zero))
练习 _[_:=_]′ (延伸)

上面的替换定义中有三条语句(ƛcaseμ) 使用了 with 语句来处理约束变量。 将上述语句的共同部分提取成一个函数,然后用共同递归重写替换的定义。

-- 请将代码写在此处。

规约

我们接下来给出 λ-演算的传值规约规则。 规约一个应用时,我们首先规约左手边,直到它变成一个值(必须是抽象); 接下来我们规约右手边,直到它变成一个值; 最后我们使用替换,把变量替换成参数。

在非正式的操作语言表达中,我们可以如下写出应用的规约规则:

L —→ L′
--------------- ξ-·₁
L · M —→ L′ · M

M —→ M′
--------------- ξ-·₂
V · M —→ V · M′

----------------------------- β-ƛ
(ƛ x ⇒ N) · V —→ N [ x := V ]

稍后给出的 Agda 版本的规则与上述相似,但是我们需要将全称量化显式地表示出来,也需要 使用谓词来表示一个是值的项。

规则可以分为两类。 兼容性规则让我们规约一个项的一部分。我们用希腊字母 ξxi)开头的规则表示。 当一个项规约到足够的时候,它将会包括一个构造子和一个解构子,在这里是 ƛ·, 我们可以直接规约。这样的规则我们用希腊字母 βbeta)表示,也被称为 β-规则

一些额外的术语:可以匹配规约规则左手边的项被称之为可规约项(Redex)。 在可规约项 (ƛ x ⇒ N) · V 中,我们把 x 叫做函数的形式参数(形参,Formal Parameter), 把 V 叫做函数应用的实际参数(实参,Actual Parameter)。 β-规约将形参用实参来替换。

如果一个项已经是一个值,它就没有可以规约的规则; 反过来说,如果一个项可以被规约,那么它就不是一个值。 我们在下一章里证明这概括了所有的情况——所以良类型的项要么可以规约要么是一个值。

对于数字来说,零不可以规约,后继可以对它的子项进行规约。 匹配表达式先将它的参数规约至一个数字,然后根据它是零还是后继选择相应的分支。 不动点会把约束变量替换成整个不动点项——这是我们唯一一处用项、而不是值进行的替换。

我们在 Agda 里这样形式化这些规则:

infix 4 _—→_

data _—→_ : Term  Term  Set where

  ξ-·₁ :  {L L′ M}
     L —→ L′
      -----------------
     L · M —→ L′ · M

  ξ-·₂ :  {V M M′}
     Value V
     M —→ M′
      -----------------
     V · M —→ V · M′

  β-ƛ :  {x N V}
     Value V
      ------------------------------
     (ƛ x  N) · V —→ N [ x := V ]

  ξ-suc :  {M M′}
     M —→ M′
      ------------------
     `suc M —→ `suc M′

  ξ-case :  {x L L′ M N}
     L —→ L′
      -----------------------------------------------------------------
     case L [zero⇒ M |suc x  N ] —→ case L′ [zero⇒ M |suc x  N ]

  β-zero :  {x M N}
      ----------------------------------------
     case `zero [zero⇒ M |suc x  N ] —→ M

  β-suc :  {x V M N}
     Value V
      ---------------------------------------------------
     case `suc V [zero⇒ M |suc x  N ] —→ N [ x := V ]

  β-μ :  {x M}
      ------------------------------
     μ x  M —→ M [ x := μ x  M ]

我们小心地设计这些规约规则,使得一个项的子项在整项被规约之前先被规约。 这被称为传值(Call-by-value)规约。

除此之外,我们规定规约的顺序是从左向右的。 这意味着规约是确定的(Deterministic):对于任何一个项,最多存在一个可以被规约至的项。 换句话说,我们的规约关系 —→ 实际上是一个函数。

这种解释项的含义的方法叫做小步操作语义(Small-step Operational Semantics)。 如果 M —→ N,我们称之为项 M 规约至项 N,也称之为项 M 步进(Step to)至项 N。 每条兼容性规则以另一条规约规则作为前提;因此每一步都会用到一条 β-规则,用零或多条兼容性规则进行调整。

小测验

下面的项步进至哪一项?

(ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x")  —→  ???
  1. (ƛ "x" ⇒ ` "x")
  2. (ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x")
  3. (ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x")

下面的项步进至哪一项?

(ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x")  —→  ???
  1. (ƛ "x" ⇒ ` "x")
  2. (ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x")
  3. (ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x") · (ƛ "x" ⇒ ` "x")

下面的项步进至哪一项?(twoᶜsucᶜ 如之前的定义)

twoᶜ · sucᶜ · `zero  —→  ???
  1. sucᶜ · (sucᶜ · `zero)
  2. (ƛ "z" ⇒ sucᶜ · (sucᶜ · ` "z")) · `zero
  3. `zero

自反传递闭包

步进并不是故事的全部。 总的来说,对于一个封闭的项,我们想要对它反复地步进,直到规约至一个值。 这样可以用定义步进关系 —→ 的自反传递闭包 —↠ 来完成。

我们以一个零或多步的步进关系的序列来定义这样的自反传递闭包,这样的形式与 Equality 章节中的等式链论证形式相似:

infix  2 _—↠_
infix  1 begin_
infixr 2 _—→⟨_⟩_
infix  3 _∎

data _—↠_ : Term  Term  Set where
  _∎ :  M
      ---------
     M —↠ M

  step—→ :  L {M N}
     M —↠ N
     L —→ M
      ---------
     L —↠ N

pattern _—→⟨_⟩_ L L—→M M—↠N = step—→ L M—↠N L—→M

begin_ :  {M N}
   M —↠ N
    ------
   M —↠ N
begin M—↠N = M—↠N

我们如下理解这个关系:

  • 对于项 M,我们可以一步也不规约而得到类型为 M —↠ M 的步骤,写作 M ∎
  • 对于项 L,我们可以使用 L —→ M 类型步进一步,再使用 M —↠ N 类型步进零或多步, 得到类型为 L —↠ N 的步骤,写作 L —→⟨ L—→M ⟩ M—↠N。其中, L—→MM—↠N 是相应类型的步骤。

在下一部分我们可以看到,这样的记法可以让我们用清晰的步骤来表示规约的例子。

我们曾在 (Equality)[Equality] 一章中用 step-≡ 和一个反转了实参顺序的语法声明定义了等式链, 这里我们同样以反转了实参顺序的模式声明来引入 step—→。 和之前一样,这能让 Agda 更高效地执行类型推断。我们后面会用到很长的规约链, 因此效率是很重要的。

我们也可以用包括 —→ 的最小的自反传递关系来定义:

data _—↠′_ : Term  Term  Set where

  step′ :  {M N}
     M —→ N
      -------
     M —↠′ N

  refl′ :  {M}
      -------
     M —↠′ M

  trans′ :  {L M N}
     L —↠′ M
     M —↠′ N
      -------
     L —↠′ N

这样的三个构造子分别表示 —↠′ 包含了 —→、自反和传递的性质。 证明两者是等价的是一个很好的练习。(的确,一者嵌入了另一者)

练习 —↠≲—↠′ (习题)

证明自反传递闭包的第一种记法嵌入了第二种记法。 为什么它们不是同构的?

-- 请将代码写在此处。

合流性

在讨论规约关系时,有一个重要的性质是合流性(Confluence)。 如果项 L 规约至两个项 M 和项 N,那么它们都可以规约至同一个项 P。 我们可以用下面的图来展示这个性质:

           L
          / \
         /   \
        /     \
       M       N
        \     /
         \   /
          \ /
           P

图中,LMN 由全称量词涵盖,而 P 由存在量词涵盖。 如果图中的每条线代表了零或多步规约步骤,这样的性质被称为合流性。 如果上面的两条线代表一步规约步骤,下面的两条线代表零或多步规约步骤, 这样的性质被称为菱形性质(Diamond Property)。用符号表示为:

postulate
  confluence :  {L M N}
     ((L —↠ M) × (L —↠ N))
      --------------------
     ∃[ P ] ((M —↠ P) × (N —↠ P))

  diamond :  {L M N}
     ((L —→ M) × (L —→ N))
      --------------------
     ∃[ P ] ((M —↠ P) × (N —↠ P))

在本章中我们讨论的规约系统是确定的。用符号表示为:

postulate
  deterministic :  {L M N}
     L —→ M
     L —→ N
      ------
     M  N

我们可以简单地证明任何确定的规约关系满足菱形性质, 任何满足菱形性质的规约关系满足合流性。 因此,我们研究的规约系统平凡地满足了合流性。

例子

我们用一个简单的例子开始。Church 数二应用于后继函数和零可以得到自然数二:
_ : twoᶜ · sucᶜ · `zero —↠ `suc `suc `zero
_ =
  begin
    twoᶜ · sucᶜ · `zero
  —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ "z"  sucᶜ · (sucᶜ · ` "z")) · `zero
  —→⟨ β-ƛ V-zero 
    sucᶜ · (sucᶜ · `zero)
  —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    sucᶜ · `suc `zero
  —→⟨ β-ƛ (V-suc V-zero) 
    `suc (`suc `zero)
  
下面的例子中我们规约二加二至四:
_ : plus · two · two —↠ `suc `suc `suc `suc `zero
_ =
  begin
    plus · two · two
  —→⟨ ξ-·₁ (ξ-·₁ β-μ) 
    (ƛ "m"  ƛ "n" 
      case ` "m" [zero⇒ ` "n" |suc "m"  `suc (plus · ` "m" · ` "n") ])
        · two · two
  —→⟨ ξ-·₁ (β-ƛ (V-suc (V-suc V-zero))) 
    (ƛ "n" 
      case two [zero⇒ ` "n" |suc "m"  `suc (plus · ` "m" · ` "n") ])
         · two
  —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    case two [zero⇒ two |suc "m"  `suc (plus · ` "m" · two) ]
  —→⟨ β-suc (V-suc V-zero) 
    `suc (plus · `suc `zero · two)
  —→⟨ ξ-suc (ξ-·₁ (ξ-·₁ β-μ)) 
    `suc ((ƛ "m"  ƛ "n" 
      case ` "m" [zero⇒ ` "n" |suc "m"  `suc (plus · ` "m" · ` "n") ])
        · `suc `zero · two)
  —→⟨ ξ-suc (ξ-·₁ (β-ƛ (V-suc V-zero))) 
    `suc ((ƛ "n" 
      case `suc `zero [zero⇒ ` "n" |suc "m"  `suc (plus · ` "m" · ` "n") ])
        · two)
  —→⟨ ξ-suc (β-ƛ (V-suc (V-suc V-zero))) 
    `suc (case `suc `zero [zero⇒ two |suc "m"  `suc (plus · ` "m" · two) ])
  —→⟨ ξ-suc (β-suc V-zero) 
    `suc `suc (plus · `zero · two)
  —→⟨ ξ-suc (ξ-suc (ξ-·₁ (ξ-·₁ β-μ))) 
    `suc `suc ((ƛ "m"  ƛ "n" 
      case ` "m" [zero⇒ ` "n" |suc "m"  `suc (plus · ` "m" · ` "n") ])
        · `zero · two)
  —→⟨ ξ-suc (ξ-suc (ξ-·₁ (β-ƛ V-zero))) 
    `suc `suc ((ƛ "n" 
      case `zero [zero⇒ ` "n" |suc "m"  `suc (plus · ` "m" · ` "n") ])
        · two)
  —→⟨ ξ-suc (ξ-suc (β-ƛ (V-suc (V-suc V-zero)))) 
    `suc `suc (case `zero [zero⇒ two |suc "m"  `suc (plus · ` "m" · two) ])
  —→⟨ ξ-suc (ξ-suc β-zero) 
    `suc (`suc (`suc (`suc `zero)))
  
我们用 Church 数规约同样的例子:
_ : plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero —↠ `suc `suc `suc `suc `zero
_ =
  begin
    (ƛ "m"  ƛ "n"  ƛ "s"  ƛ "z"  ` "m" · ` "s" · (` "n" · ` "s" · ` "z"))
      · twoᶜ · twoᶜ · sucᶜ · `zero
  —→⟨ ξ-·₁ (ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ))) 
    (ƛ "n"  ƛ "s"  ƛ "z"  twoᶜ · ` "s" · (` "n" · ` "s" · ` "z"))
      · twoᶜ · sucᶜ · `zero
  —→⟨ ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ "s"  ƛ "z"  twoᶜ · ` "s" · (twoᶜ · ` "s" · ` "z")) · sucᶜ · `zero
  —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ "z"  twoᶜ · sucᶜ · (twoᶜ · sucᶜ · ` "z")) · `zero
  —→⟨ β-ƛ V-zero 
    twoᶜ · sucᶜ · (twoᶜ · sucᶜ · `zero)
  —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ "z"  sucᶜ · (sucᶜ · ` "z")) · (twoᶜ · sucᶜ · `zero)
  —→⟨ ξ-·₂ V-ƛ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ "z"  sucᶜ · (sucᶜ · ` "z")) · ((ƛ "z"  sucᶜ · (sucᶜ · ` "z")) · `zero)
  —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    (ƛ "z"  sucᶜ · (sucᶜ · ` "z")) · (sucᶜ · (sucᶜ · `zero))
  —→⟨ ξ-·₂ V-ƛ (ξ-·₂ V-ƛ (β-ƛ V-zero)) 
    (ƛ "z"  sucᶜ · (sucᶜ · ` "z")) · (sucᶜ · (`suc `zero))
  —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc V-zero)) 
    (ƛ "z"  sucᶜ · (sucᶜ · ` "z")) · (`suc `suc `zero)
  —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    sucᶜ · (sucᶜ · `suc `suc `zero)
  —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc (V-suc V-zero))) 
    sucᶜ · (`suc `suc `suc `zero)
  —→⟨ β-ƛ (V-suc (V-suc (V-suc V-zero))) 
   `suc (`suc (`suc (`suc `zero)))
  

下一章节中,我们研究如何计算这样的规约序列。

练习 plus-example (习题)

使用规约序列,证明一加一得二。

-- 请将代码写在此处。

类型的语法

我们只有两种类型:

  • 函数:A ⇒ B
  • 自然数:`ℕ

和之前一样,我们需要使用与 Agda 不一样的名称来防止混淆。

下面是类型的 BNF 形式语法:

A, B, C  ::=  A ⇒ B | `ℕ

下面是用 Agda 的形式化:

infixr 7 _⇒_

data Type : Set where
  _⇒_ : Type  Type  Type
  `ℕ : Type

优先级

与 Agda 中一致,两个或多个参数的函数以柯里化的形式表示。 以右结合的方式定义 _⇒_、左结合的方式定义 _·_ 更加方便。 因此:

  • (`ℕ ⇒ `ℕ) ⇒ `ℕ ⇒ `ℕ 表示 ((`ℕ ⇒ `ℕ) ⇒ (`ℕ ⇒ `ℕ))
  • plus · two · two 表示 (plus · two) · two

小测验

  • 下面给出的项的类型是什么?

    ƛ "s" ⇒ ` "s" · (` "s" · `zero)

    1. (`ℕ ⇒ `ℕ) ⇒ (`ℕ ⇒ `ℕ)
    2. (`ℕ ⇒ `ℕ) ⇒ `ℕ
    3. `ℕ ⇒ (`ℕ ⇒ `ℕ)
    4. `ℕ ⇒ `ℕ ⇒ `ℕ
    5. `ℕ ⇒ `ℕ
    6. `ℕ

    在适当的情况下,可以给出多于一个答案。

  • 下面给出的项的类型是什么?

    (ƛ "s" ⇒ ` "s" · (` "s" · `zero)) · sucᶜ

    1. (`ℕ ⇒ `ℕ) ⇒ (`ℕ ⇒ `ℕ)
    2. (`ℕ ⇒ `ℕ) ⇒ `ℕ
    3. `ℕ ⇒ (`ℕ ⇒ `ℕ)
    4. `ℕ ⇒ `ℕ ⇒ `ℕ
    5. `ℕ ⇒ `ℕ
    6. `ℕ

    在适当的情况下,可以给出多于一个答案。

赋型

语境

在规约时,我们只讨论封闭的项,但是在赋型时,我们必须考虑带有自由变量的项。 给一个项赋型时,我们必须先给它的子项赋型。而在给一个抽象的抽象体赋型时, 抽象的约束变量在抽象体内部是自由的。

语境(Context)将变量和类型联系在一起。 我们用 ΓΔ 来表示语境。 我们用 表示空的语境,用 Γ , x ⦂ A 表示扩充 Γ ,将变量 x 对应至类型 A。 例如:

  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ

这个语境将变量 "s" 对应至类型 `ℕ ⇒ `ℕ, 将变量 "z" 对应至类型 `ℕ

语境如下形式化:

infixl 5  _,_⦂_

data Context : Set where
       : Context
  _,_⦂_ : Context  Id  Type  Context
练习 Context-≃ (习题)

证明 ContextList (Id × Type) 同构。

例如,如下的语境

∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ

和如下的列表相关。

[ ⟨ "z" , `ℕ ⟩ , ⟨ "s" , `ℕ ⇒ `ℕ ⟩ ]
-- 请将代码写在此处。

查询判断

我们使用两种判断。第一种写作

Γ ∋ x ⦂ A

表示在语境 Γ 中变量 x 的类型是 A。这样的判断叫做查询(Lookup)判断。 例如,

  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ∋ "z" ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ∋ "s" ⦂ `ℕ ⇒ `ℕ

分别给出了变量 "z""s" 对应的类型。 我们使用符号 (读作 “ni”,反写的 “in”),因为 Γ ∋ x ⦂ A 与查询 x ⦂ A 是否在与 Γ 对应的列表中存在相似。

如果语境中有相同名称的两个变量,那么查询会返回被约束的最近的变量,它遮盖(Shadow) 了另一个变量。例如:

  • ∅ , "x" ⦂ `ℕ ⇒ `ℕ , "x" ⦂ `ℕ ∋ "x" ⦂ `ℕ.

在这里 "x" ⦂ `ℕ ⇒ `ℕ"x" ⦂ `ℕ 遮盖了。

我们如下形式化查询:
infix  4  _∋_⦂_

data _∋_⦂_ : Context  Id  Type  Set where

  Z :  {Γ x A}
      ------------------
     Γ , x  A  x  A

  S :  {Γ x y A B}
     x  y
     Γ  x  A
      ------------------
     Γ , y  B  x  A

构造子 ZS 大致与列表包含关系 _∈_herethere 构造子对应。 但是构造子 S 多取一个参数,来保证查询时我们不会查询一个被遮盖的同名变量。

S 构造子会比较麻烦,因为每次都需要提供 x ≢ y 的证明。例如:

_ :  , "x"  `ℕ  `ℕ , "y"  `ℕ , "z"  `ℕ  "x"  `ℕ  `ℕ
_ = S (λ()) (S (λ()) Z)

取而代之的是,我们在类型检查时可以使用以互映证明来检查不等性的「智慧构造子」:

S′ :  {Γ x y A B}
    {x≢y : False (x  y)}
    Γ  x  A
     ------------------
    Γ , y  B  x  A

S′ {x≢y = x≢y} x = S (toWitnessFalse x≢y) x

赋型判断

第二种判断写作

Γ ⊢ M ⦂ A

表示在语境 Γ 中,项 M 有类型 A。 语境 ΓM 中的所有自由变量提供了类型。 例如:

  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "z" ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "s" ⦂ `ℕ ⇒ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "s" · ` "z" ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "s" · (` "s" · ` "z") ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ ⊢ ƛ "z" ⇒ ` "s" · (` "s" · ` "z") ⦂ `ℕ ⇒ `ℕ
  • ∅ ⊢ ƛ "s" ⇒ ƛ "z" ⇒ ` "s" · (` "s" · ` "z") ⦂ (`ℕ ⇒ `ℕ) ⇒ `ℕ ⇒ `ℕ
赋型可以如下形式化:
infix  4  _⊢_⦂_

data _⊢_⦂_ : Context  Term  Type  Set where

  -- Axiom
  ⊢` :  {Γ x A}
     Γ  x  A
      -----------
     Γ  ` x  A

  -- ⇒-I
  ⊢ƛ :  {Γ x N A B}
     Γ , x  A  N  B
      -------------------
     Γ  ƛ x  N  A  B

  -- ⇒-E
  _·_ :  {Γ L M A B}
     Γ  L  A  B
     Γ  M  A
      -------------
     Γ  L · M  B

  -- ℕ-I₁
  ⊢zero :  {Γ}
      --------------
     Γ  `zero  `ℕ

  -- ℕ-I₂
  ⊢suc :  {Γ M}
     Γ  M  `ℕ
      ---------------
     Γ  `suc M  `ℕ

  -- ℕ-E
  ⊢case :  {Γ L M x N A}
     Γ  L  `ℕ
     Γ  M  A
     Γ , x  `ℕ  N  A
      -------------------------------------
     Γ  case L [zero⇒ M |suc x  N ]  A

  ⊢μ :  {Γ x M A}
     Γ , x  A  M  A
      -----------------
     Γ  μ x  M  A

赋型规则由对应的项的构造子来命名。

大多数规则有第二个名字,从逻辑中的惯例得到。规则的名称也可以用类型的连接符中得到, 引入和消去连接符分别用 -I-E 表示。 我们从上往下阅读时,引入和消去的规则一目了然:前者引入了一个带有连接符的式子, 其出现在结论中,而不是条件中;后者消去了带有连接符的式子,其出现在条件中,而不是结论中。 引入规则表示了如何构造一个给定类型的值(抽象产生函数、零和后继产生自然数),而消去规则 表示了如何解构一个给定类型的值(应用使用函数,匹配表达式使用自然数)。

另外需要注意的是有三处地方(⊢ƛ⊢case⊢μ),语境被 x 和相应的类型 所扩充,对应着三处约束变量的引入。

这些规则是确定的,对于每一项至多有一条规则使用。

类型推导的例子

类型推导对应着树。在非正式的记法中,下面是 Church 数二的类型推导:

                        ∋s                     ∋z
                        ------------------ ⊢`  -------------- ⊢`
∋s                      Γ₂ ⊢ ` "s" ⦂ A ⇒ A     Γ₂ ⊢ ` "z" ⦂ A
------------------ ⊢`   ------------------------------------- _·_
Γ₂ ⊢ ` "s" ⦂ A ⇒ A      Γ₂ ⊢ ` "s" · ` "z" ⦂ A
---------------------------------------------- _·_
Γ₂ ⊢ ` "s" · (` "s" · ` "z") ⦂ A
-------------------------------------------- ⊢ƛ
Γ₁ ⊢ ƛ "z" ⇒ ` "s" · (` "s" · ` "z") ⦂ A ⇒ A
------------------------------------------------------------- ⊢ƛ
Γ ⊢ ƛ "s" ⇒ ƛ "z" ⇒ ` "s" · (` "s" · ` "z") ⦂ (A ⇒ A) ⇒ A ⇒ A

其中 ∋s∋z 是下面两个推导的简写:

             ---------------- Z
"s" ≢ "z"    Γ₁ ∋ "s" ⦂ A ⇒ A
----------------------------- S       ------------- Z
Γ₂ ∋ "s" ⦂ A ⇒ A                       Γ₂ ∋ "z" ⦂ A

其中 Γ₁ = Γ , "s" ⦂ A ⇒ AΓ₂ = Γ , "s" ⦂ A ⇒ A , "z" ⦂ A。 给出的推导对于任意的 ΓA 有效,例如, 我们可以取 ΓA`ℕ

上面的推导可以如下用 Agda 形式化:
Ch : Type  Type
Ch A = (A  A)  A  A

⊢twoᶜ :  {Γ A}  Γ  twoᶜ  Ch A
⊢twoᶜ = ⊢ƛ (⊢ƛ (⊢` ∋s · (⊢` ∋s · ⊢` ∋z)))
  where
  ∋s = S′ Z
  ∋z = Z
下面是针对二加二的赋型:
⊢two :  {Γ}  Γ  two  `ℕ
⊢two = ⊢suc (⊢suc ⊢zero)

⊢plus :  {Γ}  Γ  plus  `ℕ  `ℕ  `ℕ
⊢plus = ⊢μ (⊢ƛ (⊢ƛ (⊢case (⊢` ∋m) (⊢` ∋n)
         (⊢suc (⊢` ∋+ · ⊢` ∋m′ · ⊢` ∋n′)))))
  where
  ∋+  = S′ (S′ (S′ Z))
  ∋m  = S′ Z
  ∋n  = Z
  ∋m′ = Z
  ∋n′ = S′ Z

⊢2+2 :   plus · two · two  `ℕ
⊢2+2 = ⊢plus · ⊢two · ⊢two

与之前的例子不同,我们以任意语境,而不是空语境来赋型。 这让我们能够在其他除了顶层之外的语境中使用这个推导。 这里的查询判断 ∋m∋m′ 指代两个变量 "m" 的绑定。 作为对比,查询判断 ∋n∋n′ 指代同一个变量 "n" 的绑定,但是查询的语境不同, 第一次 "n" 出现在在语境的最后,第二次在 "m" 之后。

对 Church 数赋型的余下推导如下:
⊢plusᶜ :  {Γ A}  Γ   plusᶜ  Ch A  Ch A  Ch A
⊢plusᶜ = ⊢ƛ (⊢ƛ (⊢ƛ (⊢ƛ (⊢` ∋m · ⊢` ∋s · (⊢` ∋n · ⊢` ∋s · ⊢` ∋z)))))
  where
  ∋m = S′ (S′ (S′ Z))
  ∋n = S′ (S′ Z)
  ∋s = S′ Z
  ∋z = Z

⊢sucᶜ :  {Γ}  Γ  sucᶜ  `ℕ  `ℕ
⊢sucᶜ = ⊢ƛ (⊢suc (⊢` ∋n))
  where
  ∋n = Z

⊢2+2ᶜ :   plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero  `ℕ
⊢2+2ᶜ = ⊢plusᶜ · ⊢twoᶜ · ⊢twoᶜ · ⊢sucᶜ · ⊢zero

与 Agda 交互

可以交互式地构造类型推导。 从声明开始:

⊢sucᶜ : ∅ ⊢ sucᶜ ⦂ `ℕ ⇒ `ℕ
⊢sucᶜ = ?

使用 C-c C-l 让 Agda 创建一个洞,并且告诉我们期望的类型:

⊢sucᶜ = { }0
?0 : ∅ ⊢ sucᶜ ⦂ `ℕ ⇒ `ℕ

现在使用 C-c C-r 来填补这个洞。Agda 注意到 sucᶜ 最外层的项是 ƛ,应该使用 ⊢ƛ 来赋型。 ⊢ƛ 规则需要一个变量,用一个新的洞表示:

⊢sucᶜ = ⊢ƛ { }1
?1 : ∅ , "n" ⦂ `ℕ ⊢ `suc ` "n" ⦂ `ℕ

再次使用 C-c C-r 来填补洞:

⊢sucᶜ = ⊢ƛ (⊢suc { }2)
?2 : ∅ , "n" ⦂ `ℕ ⊢ ` "n" ⦂ `ℕ

再来一次:

⊢sucᶜ = ⊢ƛ (⊢suc (⊢` { }3))
?3 : ∅ , "n" ⦂ `ℕ ∋ "n" ⦂ `ℕ

再次尝试使用 C-c C-r 得到下面的消息:

Don't know which constructor to introduce of Z or S

我们使用填入 Z。如果我们使用 C-c C-space,Agda 证实我们完成了:

⊢sucᶜ = ⊢ƛ (⊢suc (⊢` Z))

我们也可以使用 C-c C-a,用 Agsy 来自动完成。

Inference 章节中,我们会展示如何使用 Agda 来直接计算出类型推导。

查询是函数

查询关系 Γ ∋ x ⦂ A 是一个函数。 对于所有的 Γx, 至多有一个 A 满足这个判断:

∋-functional :  {Γ x A B}  Γ  x  A  Γ  x  B  A  B
∋-functional Z        Z          =  refl
∋-functional Z        (S x≢ _)   =  ⊥-elim (x≢ refl)
∋-functional (S x≢ _) Z          =  ⊥-elim (x≢ refl)
∋-functional (S _ ∋x) (S _ ∋x′)  =  ∋-functional ∋x ∋x′

赋型关系 Γ ⊢ M ⦂ A 不是一个函数。例如,在任何 Γ 中 项 ƛ "x" ⇒ ` "x" 有类型 A ⇒ AA 为任何类型。

非例子

我们也可以证明一些项不是可赋型的。例如,我们接下来证明项 `zero · `suc `zero 是不可赋型的。 原因在于我们需要使得 `zero 既是一个函数又是一个自然数。

nope₁ :  {A}  ¬ (  `zero · `suc `zero  A)
nope₁ (() · _)

第二个例子,我们证明项 ƛ "x" ⇒ ` "x" · ` "x" 是不可赋型的。 原因在于我们需要满足 A ⇒ B ≡ A 的两个类型 AB

nope₂ :  {A}  ¬ (  ƛ "x"  ` "x" · ` "x"  A)
nope₂ (⊢ƛ (⊢` ∋x · ⊢` ∋x′))  =  contradiction (∋-functional ∋x ∋x′)
  where
  contradiction :  {A B}  ¬ (A  B  A)
  contradiction ()
小测验

对于下面的每一条,如果可以推导,给出类型 A,否则说明为什么这样的 A 不存在。

  1. ∅ , "y" ⦂ `ℕ ⇒ `ℕ , "x" ⦂ `ℕ ⊢ ` "y" · ` "x" ⦂ A
  2. ∅ , "y" ⦂ `ℕ ⇒ `ℕ , "x" ⦂ `ℕ ⊢ ` "x" · ` "y" ⦂ A
  3. ∅ , "y" ⦂ `ℕ ⇒ `ℕ ⊢ ƛ "x" ⇒ ` "y" · ` "x" ⦂ A

对于下面的每一条,如果可以推导,给出类型 ABC,否则说明为什么这样的类型 不存在。

  1. ∅ , "x" ⦂ A ⊢ ` "x" · ` "x" ⦂ B
  2. ∅ , "x" ⦂ A , "y" ⦂ B ⊢ ƛ "z" ⇒ ` "x" · (` "y" · ` "z") ⦂ C
练习 ⊢mul (推荐)

使用你之前写出的项 mul,给出其良类型的推导。

-- 请将代码写在此处。
练习 ⊢mulᶜ (习题)

使用你之前写出的项 mulᶜ,给出其良类型的推导。

-- 请将代码写在此处。

Unicode

本章中使用了以下 Unicode:

⇒  U+21D2  RIGHTWARDS DOUBLE ARROW (\=>)
ƛ  U+019B  LATIN SMALL LETTER LAMBDA WITH STROKE (\Gl-)
·  U+00B7  MIDDLE DOT (\cdot)
≟  U+225F  QUESTIONED EQUAL TO (\?=)
—  U+2014  EM DASH (\em)
↠  U+21A0  RIGHTWARDS TWO HEADED ARROW (\rr-)
ξ  U+03BE  GREEK SMALL LETTER XI (\Gx or \xi)
β  U+03B2  GREEK SMALL LETTER BETA (\Gb or \beta)
Γ  U+0393  GREEK CAPITAL LETTER GAMMA (\GG or \Gamma)
≠  U+2260  NOT EQUAL TO (\=n or \ne)
∋  U+220B  CONTAINS AS MEMBER (\ni)
∅  U+2205  EMPTY SET (\0)
⊢  U+22A2  RIGHT TACK (\vdash or \|-)
⦂  U+2982  Z NOTATION TYPE COLON (\:)
😇  U+1F607  SMILING FACE WITH HALO
😈  U+1F608  SMILING FACE WITH HORNS

我们用短划 和箭头 来构造规约 —→。 自反传递闭包 —↠ 也类似。

Properties: 可进性与保型性

module plfa.part2.Properties where

本章涵盖了上一章所介绍的简单类型 λ-演算的性质。 在这些性质中最为重要的是可进性(Progress)与保型性(Preservation)。 我们将在稍后介绍它们,并展示如何通过组合它们来使 Agda 为我们计算归约序列。

导入

open import Relation.Binary.PropositionalEquality
  using (_≡_; _≢_; refl; sym; cong; cong₂)
open import Data.String using (String; _≟_)
open import Data.Nat using (; zero; suc)
open import Data.Empty using (; ⊥-elim)
open import Data.Product
  using (_×_; proj₁; proj₂; ; ∃-syntax)
  renaming (_,_ to ⟨_,_⟩)
open import Data.Sum using (_⊎_; inj₁; inj₂)
open import Relation.Nullary using (¬_; Dec; yes; no)
open import Function using (_∘_)
open import plfa.part1.Isomorphism
open import plfa.part2.Lambda

简介

在上一章中介绍了简单类型的 λ-演算,包括闭项、作为值的项、将一个项 归约为另一个项和良类型的项的概念。

最终,我们将要展示我们能够通过持续地对一个项做归约,直到它达到一个值。 例如,在上一章中我们展示了二加二的和是四,

plus · two · two  —↠  `suc `suc `suc `suc `zero

而这是通过一长链的归约步骤证明的,结束于链最右侧的值。 链上的每一个项都具有相同的类型,即 `ℕ。 我们还见到了第二个类似的例子,涉及了 Church 表示法表示的数字。

我们可能会希望任意一个项要么是一个值,要么可以进行一步规约。 我们将会看到,此性质并不对所有项都成立,但对于所有良类型的闭项成立。

可进性:如果有 ∅ ⊢ M ⦂ A,那么要么项 M 是一个值,要么存在一个项 N 使 得 M —→ N 成立。

所以要么我们有一个值,这时我们已经完成了规约;要么我们可以进行一步规约。 当处于后者的情况时,我们想要再一次应用可进性。 但这样做需要我们首先知道通过规约得到的项本身是良类型的闭项。 事实上,只要我们规约的起点是一个良类型的闭项,所得到的项就满足这个性质。

保型性:如果 ∅ ⊢ M ⦂ AM —→ N,那么 ∅ ⊢ N ⦂ A

这给予我们一种自动化求值的策略。 从一个良类型的闭项开始。 由可进性,它要么是一个值,于是求值结束了;要么可以被规约为另一个项。 由保型性,所以得到的另一个项本身也是一个良类型的闭项。 重复这一过程。 我们要么会陷入永久的循环中,此时求值过程将不会停机; 要么最终会得到一个被确保是闭项且类型与原始项相同的值。 接下来我们将这种策略转化为 Agda 代码,以计算 plus · two · two 和所 对应 Church 表示法表示数字的变体的规约序列。

(这一章启发自《软件基础》(Software Foundations)/《程序语言基础》(Programming Language Foundations)中对应的 StlcProp 一章。 事实上我们技术选择中的一个——通过显示地引入一条判断 Γ ∋ x ⦂ A, 而不是将语境视作为一个从标识符映射到类型的函数——简化了开发过程。 特别地,我们不需要额外地去归纳定义关系 appears_free_in 就可以证明替换保留了类型。)

值无法被规约

我们从一个简单的观察开始。值无法被规约:

V¬—→ :  {M N}
   Value M
    ----------
   ¬ (M —→ N)
V¬—→ V-ƛ        ()
V¬—→ V-zero     ()
V¬—→ (V-suc VM) (ξ-suc M—→N) = V¬—→ VM M—→N

我们考虑值可能处于的三种情况:

  • 如果它是一个 λ-抽象,那么不会匹配任何规约规则

  • 如果它是零,也不会匹配任何规约规则

  • 如果它是一个数的后继,那么它可能可以与规则 ξ-suc 相匹配, 但由归纳证明它作为值本身还能进行规约的情况是不可能发生的。

作为推论,可以再进行规约的项不是值:

—→¬V :  {M N}
   M —→ N
    ---------
   ¬ Value M
—→¬V M—→N VM  =  V¬—→ VM M—→N

如果我们将这些否定形式展开,可以得到

V¬—→ : ∀ {M N} → Value M → M —→ N → ⊥
—→¬V : ∀ {M N} → M —→ N → Value M → ⊥

可知原命题与推论是同一个函数,只是交换了参数顺序。

练习 Canonical-≃ (实践)

良类型的式子都属于少数几种标准式(Canonical Form)中的一种。 标准式提供了一种类似于 Value 的关系,关联值和它们所属的类型。 一个 λ-表达式一定属于函数类型,同时零和后继表达式都属于自然数。 更进一步说,此时函数的函数体必须在只包含它的约束变量的语境中是良类型的, 后继的参数本身也必须是标准式:

infix  4 Canonical_⦂_

data Canonical_⦂_ : Term  Type  Set where

  C-ƛ :  {x A N B}
      , x  A  N  B
      -----------------------------
     Canonical (ƛ x  N)  (A  B)

  C-zero :
      --------------------
      Canonical `zero  `ℕ

  C-suc :  {V}
     Canonical V  `ℕ
      ---------------------
     Canonical `suc V  `ℕ

证明 Canonical V ⦂ A(∅ ⊢ V ⦂ A) × (Value V) 同构, 也就是标准式即良类型的值。

-- 请将代码写在此处。

可进性

我们可能希望任意一个项要么是值,要么可以进行一步规约。 但并不是所有情况都是这样。考虑这样的项

`zero · `suc `zero

既不是一个值也无法进行一步规约。另外,如果 "s" ⦂ `ℕ ⇒ `ℕ,那么项

 ` "s" · `zero

也无法被规约,因为我们不知道哪个函数被绑定到了自由变量 "s" 上。 上述例子中的第一个项是不良类型的,第二个项包含一个自由变量。 只有良类型的闭项才有如下求证的性质:

可进性:如果有 ∅ ⊢ M ⦂ A,那么要么项 M 是一个值,要么存在一个项 N 使 得 M —→ N 成立。

要陈述这一性质,我们首先需要引入一个关系来刻画什么样的项 M 才是进行的:

data Progress (M : Term) : Set where

  step :  {N}
     M —→ N
      ----------
     Progress M

  done :
      Value M
      ----------
     Progress M

一个进行的项 M 要么可以进行一步规约,这意味着存在一个项 N 使得 M —→ N, 要么已经完成了规约,这意味着 M 是一个值。

如果一个项在空语境中是良类型的,那么它满足可进性:

progress :  {M A}
     M  A
    ----------
   Progress M
progress (⊢` ())
progress (⊢ƛ ⊢N)                            =  done V-ƛ
progress (⊢L · ⊢M) with progress ⊢L
... | step L—→L′                            =  step (ξ-·₁ L—→L′)
... | done V-ƛ with progress ⊢M
...   | step M—→M′                          =  step (ξ-·₂ V-ƛ M—→M′)
...   | done VM                             =  step (β-ƛ VM)
progress ⊢zero                              =  done V-zero
progress (⊢suc ⊢M) with progress ⊢M
...  | step M—→M′                           =  step (ξ-suc M—→M′)
...  | done VM                              =  done (V-suc VM)
progress (⊢case ⊢L ⊢M ⊢N) with progress ⊢L
... | step L—→L′                            =  step (ξ-case L—→L′)
... | done (V-zero)                         =  step β-zero
... | done (V-suc VL)                       =  step (β-suc VL)
progress (⊢μ ⊢M)                            =  step β-μ

我们对这个项良类型的论据做归纳。 让我们首先分析前三个情况:

  • 这个项不可能是变量,因为没有变量在空语境中是良类型的。

  • 如果这个项是一个 λ-抽象,可知它是一个值。

  • 如果这个项是一个函数应用 L · M,则考虑对项 L 良类型的推导过程递归应用可进性:

    • 如果这个项还能够进行一步规约,我们就有了 L —→ L′ 的论据,再由 ξ-·₁, 可知原来的项进行到 L′ · M

    • 如果这个项的规约结束了,我们就有了 L 是一个值的论据。 则考虑对项 L 良类型的推导过程递归应用可进性:

      • 如果这个项还能够进行一步规约,我们就有了 M —→ M′ 的论据,再由 ξ-·₂, 可知原来的项进行到 L′ · M。要应用规约步骤 ξ-·₂ 需要我们提供项 L 是 一个值的论据,而之前对子项可进性的分析已经提供了需要的证明。
      • 如果这个项的规约结束了,我们便有了项 M 是一个值的论据。 因此原来的项可以使用 β-ƛ 来进行一步规约。

剩下的情况都很类似。如果我们由归纳得到了一个可以继续进行规约的 情况 step 则应用一条 ξ 规则;如果得到的是已经完成规约的 情况 done 则要么我们已经得到了一个值,要么应用一条 β 规则。 对于不动点,由于可以直接应用所对应的 β 规则,不需要再做归纳。

由于我们在处理 done 的情况之前先考虑了 step 的情况, 我们的代码读起来还算简洁。当然我们也可以将其以相反的顺序写, 但这样 ... 的简写形式便不再起作用了,我们便需要在所有情况 都写出所有的参数。一般来说,推荐先考虑简单的情况(也就是此 处的 step),然后再考虑较难的情况(此处的 done)。 如果两个情况都比较难,此时你可能不得不将 ... 展开,或者引 入一些辅助函数。

也可以用析取和存在量化来形式化可进性, 而不是为 Progress M 定义一个数据类型:

postulate
  progress′ :  M {A}    M  A  Value M  ∃[ N ](M —→ N)

但这会导致证明变得不那么明晰易懂。 比起有助于记忆的 donestep,现在我们只能用 inj₁inj₂; 同时项 N 也不再是隐式的,从而我们需要将其完整写出。 当遇到 β-ƛ 的情况时,需要我们对 λ-表达式 L 做匹配,以决定它的约束变量和函数体, 也就是形如 ƛ x ⇒ N 的形式,从而我们才能证明项 L · M 规约到了 N [ x := M ]

练习 Progress-≃(实践)

证明 Progress MValue M ⊎ ∃[ N ](M —→ N) 是同构的。

-- 请将代码写在此处。
练习 progress′(实践)

补全 progress′ 的证明,并与之前 progress 的证明相对比。

-- 请将代码写在此处。
练习 value?(实践)

通过将 progress—→¬V 相组合, 写一个程序判断一个良类型的项是否是一个值:

postulate
  value? :  {A M}    M  A  Dec (Value M)

证明保型性前的准备工作

规约过程保持类型是我们所期望去证明的另一个性质, 而事实上这需要做一定程度的准备工作。 在证明中有三个关键步骤。

第一步是证明重命名保持类型。

重命名: 考虑两个语境 ΓΔ,满足 Γ 中出现的每个变量同时 也在 Δ 中出现且具有相同的类型。那么如果一个项在语境 Γ 中 是可赋型的,它在 Δ 中也具有同样的类型。

用符号表示为:

∀ {x A} → Γ ∋ x ⦂ A  →  Δ ∋ x ⦂ A
---------------------------------
∀ {M A} → Γ ⊢ M ⦂ A  →  Δ ⊢ M ⦂ A

接下来是三个重要的结论。 弱化(Weaken)引理断言说如果一个项在空语境中是良类型的,那么它在任意语境中都是良类型的。 去除(Drop)引理断言说如果一个项在给定语境中是良类型的,且此语境中同一个变量出现了两次, 此时去除语境中被遮盖的变量,这个项仍然是良类型的。 交换(Swap)引理断言说如果一个项在给定语境中是良类型的,那么在通过交换语境中两个变量后得到的 语境中,这个项仍然是良类型的。

(重命名与《软件基础》中出现的 context invariance 引理类似, 但在这里既不需要先定义 appears_free_in 引理,也不需要 free_in_context 引理。)

证明的第二步是论述替换保持类型。

替换: 如果我们有一个类型为 A 的闭项 V,同时假定变量 x 的类型是 A、项 N 的类型是 B。 此时用项 V 替换掉项 N 中的变量 x 得到的项的类型也为 B

用符号表示为:

∅ ⊢ V ⦂ A
Γ , x ⦂ A ⊢ N ⦂ B
--------------------
Γ ⊢ N [ x := V ] ⦂ B

此结论不依赖于项 V 是一个值,但要求 V 是一个闭项; 回忆我们之所以只关注闭项的替换,是为了避免重命名约束变量。 我们将要做替换的项在语境 Γ 扩充上一个变量 x 中是良类型的; 同时完成替换后的项在语境 Γ 中是良类型的。

这条引理证明了替换与赋型是可组合的: 独立地对各组件赋型保证组合的结果也是良类型的。

第三步是证明保型性。

保型性: 如果 ∅ ⊢ M ⦂ AM —→ N,那么 ∅ ⊢ N ⦂ A

我们对所有可能的规约步骤进行归纳来证明保型性, 在证明的过程中替换引理起到了重要的作用, 它论证了在替换过程中用到的每一条 β-规则都保留了类型。

现在继续我们的三步走。

重命名

我们常常会需要去 「重建」 一个类型推演过程, 也就是将一个类型推演 Γ ⊢ M ⦂ A 替换为对应的推演 Δ ⊢ M ⦂ A。 我们能够这样做的前提是每个在语境 Γ 中出现的变量同时也出现在语境 Δ 中, 且它们的类型相同。

赋型的三条规则(λ-抽象、对自然数分项与不动点)都有假设来拓展语境以 包含一个约束变量。三条规则的每一条中都有语境 Γ 都出现在结论中,同时 拓展后的语境 Γ , x ⦂ A 出现在假设中。对于 λ-表达式,也就是:

Γ , x ⦂ A ⊢ N ⦂ B
------------------- ⊢ƛ
Γ ⊢ ƛ x ⇒ N ⦂ A ⇒ B

对自然数分项和不动点亦是如此。 要处理这类情况,我们首先证明一条论述 「如果一个语境和另一个语境之间存在映射, 在对两者添加同一个变量后映射仍然存在」 的引理:

ext :  {Γ Δ}
   (∀ {x A}              Γ  x  A          Δ  x  A)
    -----------------------------------------------------
   (∀ {x y A B}  Γ , y  B  x  A  Δ , y  B  x  A)
ext ρ Z           =  Z
ext ρ (S x≢y ∋x)  =  S x≢y (ρ ∋x)

ρ 为这个映射,它将变量 x 出现在语境 Γ 中的论据映射到 变量 x 出现在语境 Δ 中的论据。 这是由分类讨论变量 x 出现在拓展后语境 Γ , y ⦂ B 的论据证明的:

  • 如果变量 x 与变量 y 相同,我们使用 Z 来访问 拓展后 Γ 中的最后一个变量;类似地,访问拓展后 Δ 中 的最后一个变量也是通过使用 Z 来完成的。
  • 如果 xy 不相同,我们则使用 S 来跳过拓展后 Γ 中的最后一个变量, 在这里 x≢y 即是变量 xy 不相同的论据,同时 ∋x 是变量 x 出现在 语境 Γ 中的论据;类似地,我们使用 S 来跳过拓展后 Δ 的最后一个变量, 应用 ρ 来寻找变量 x 出现在语境 Δ 中的论据。

有了拓展引理为我们所用,就可以直接写出重命名保持赋型的证明了:

rename :  {Γ Δ}
   (∀ {x A}  Γ  x  A  Δ  x  A)
    ----------------------------------
   (∀ {M A}  Γ  M  A  Δ  M  A)
rename ρ (⊢` ∋w)    =  ⊢` (ρ ∋w)
rename ρ (⊢ƛ ⊢N)    =  ⊢ƛ (rename (ext ρ) ⊢N)
rename ρ (⊢L · ⊢M)  =  (rename ρ ⊢L) · (rename ρ ⊢M)
rename ρ ⊢zero      =  ⊢zero
rename ρ (⊢suc ⊢M)  =  ⊢suc (rename ρ ⊢M)
rename ρ (⊢case ⊢L ⊢M ⊢N)
                    =  ⊢case (rename ρ ⊢L) (rename ρ ⊢M) (rename (ext ρ) ⊢N)
rename ρ (⊢μ ⊢M)    =  ⊢μ (rename (ext ρ) ⊢M)

像之前一样,令 ρ 为 「变量 x 出现在语境 Γ 中的论据」 至 「变量 x 出现在 Δ 的论据」 的映射。 我们对项 M 在语境 Γ 中是良赋型的论据做归纳。我们首先来分析前三种情况:

  • 如果项是一个变量,那么对该变量出现在语境 Γ 的论据应用 ρ 就能得到 所对应的该变量出现在 Δ 的证明。
  • 如果项是一个 λ-抽象,首先使用之前证明的引理来无误地拓展映射 ρ, 然后通过归纳重命名这个 λ-抽象的函数体。
  • 如果项是函数应用的形式,则通过归纳以重命名函数与对应的参数。

对剩下情况的证明大致相同,都是通过对各个子项做归纳,并且在构造 引入一个约束变量时拓展映射。

由于做归纳的对象是这个项良赋型的推演过程,拓展语境不会使得归纳假设 无效。等价地,此时的递归会停机,这是由于第二个参数在递归过程中总是变 小一些,尽管第一个参数有时候变大一些。

我们有三条重要推论,每条都是通过构造一个恰当的语境间映射证明的:

第一,一个闭项可以被弱化到任意语境:

weaken :  {Γ M A}
     M  A
    ----------
   Γ  M  A
weaken {Γ} ⊢M = rename ρ ⊢M
  where
  ρ :  {z C}
       z  C
      ---------
     Γ  z  C
  ρ ()

这里的映射 ρ 是平凡的,由于在空语境 中不会出现可能的参数。

第二,如果语境中的最后两个变量相等,我们就可以去除掉被遮盖的一个:

drop :  {Γ x M A B C}
   Γ , x  A , x  B  M  C
    --------------------------
   Γ , x  B  M  C
drop {Γ} {x} {M} {A} {B} {C} ⊢M = rename ρ ⊢M
  where
  ρ :  {z C}
     Γ , x  A , x  B  z  C
      -------------------------
     Γ , x  B  z  C
  ρ Z                 =  Z
  ρ (S x≢x Z)         =  ⊥-elim (x≢x refl)
  ρ (S z≢x (S _ ∋z))  =  S z≢x ∋z

在这里映射 ρ 不可能被较里出现的 x 调用,由于它被较外的出现遮盖了。 忽略掉在首位的 x 的情况只可能发生在当前寻找的变量不同于 x 时 (由 x≢xz≢x 论证),但如果变量在第二个位置被发现了, 并且也包含 x,便会导出矛盾(由 x≢x refl 论证)。

第三,如果语境中的最后两个变量不同,我们可以交换它们:

swap :  {Γ x y M A B C}
   x  y
   Γ , y  B , x  A  M  C
    --------------------------
   Γ , x  A , y  B  M  C
swap {Γ} {x} {y} {M} {A} {B} {C} x≢y ⊢M = rename ρ ⊢M
  where
  ρ :  {z C}
     Γ , y  B , x  A  z  C
      --------------------------
     Γ , x  A , y  B  z  C
  ρ Z                   =  S x≢y Z
  ρ (S z≢x Z)           =  Z
  ρ (S z≢x (S z≢y ∋z))  =  S z≢y (S z≢x ∋z)

在这里重命名将语境的最后一个变量映射到倒数第二个,反之亦然。 第一行负责将 x 从语境的最后一个位置移动到倒数第二个,并且 将 y 置于最后,这要求提供 x ≢ y 的论据。

替换

证明保型性的关键——也正是证明最具有技巧性的部分——是 一条证明替换保持赋型的引理。

回忆为了避免重命名约束变量,替换被限制为只替换闭项。 我们所定义的替换并没有强加这一约束,而是由一条断言替换 保持赋型的引理刻画的。

我们所关注的是规约闭项,这意味着每当我们应用 β-规约时, 所替换的项只含有一个自由变量(也就是所对应 λ-抽象、 对自然数分项或不动点表达式的绑定变量)。然而,替换是通过 递归定义的,在我们逐层深入项时遇到的绑定变量增长了语境。 所以为了进行归纳,我们需要一个任意的语境 Γ,正如这条引理 中所陈述的。

下面是替换保持赋型的形式化陈述及其证明:

subst :  {Γ x N V A B}
     V  A
   Γ , x  A  N  B
    --------------------
   Γ  N [ x := V ]  B
subst {x = y} ⊢V (⊢` {x = x} Z) with x  y
... | yes _         =  weaken ⊢V
... | no  x≢y       =  ⊥-elim (x≢y refl)
subst {x = y} ⊢V (⊢` {x = x} (S x≢y ∋x)) with x  y
... | yes refl      =  ⊥-elim (x≢y refl)
... | no  _         =  ⊢` ∋x
subst {x = y} ⊢V (⊢ƛ {x = x} ⊢N) with x  y
... | yes refl      =  ⊢ƛ (drop ⊢N)
... | no  x≢y       =  ⊢ƛ (subst ⊢V (swap x≢y ⊢N))
subst ⊢V (⊢L · ⊢M)  =  (subst ⊢V ⊢L) · (subst ⊢V ⊢M)
subst ⊢V ⊢zero      =  ⊢zero
subst ⊢V (⊢suc ⊢M)  =  ⊢suc (subst ⊢V ⊢M)
subst {x = y} ⊢V (⊢case {x = x} ⊢L ⊢M ⊢N) with x  y
... | yes refl      =  ⊢case (subst ⊢V ⊢L) (subst ⊢V ⊢M) (drop ⊢N)
... | no  x≢y       =  ⊢case (subst ⊢V ⊢L) (subst ⊢V ⊢M) (subst ⊢V (swap x≢y ⊢N))
subst {x = y} ⊢V (⊢μ {x = x} ⊢M) with x  y
... | yes refl      =  ⊢μ (drop ⊢M)
... | no  x≢y       =  ⊢μ (subst ⊢V (swap x≢y ⊢M))

我们对 N 在由 x 拓展 Γ 后得到的语境中是良赋型的论据做归纳。

首先我们注意到一个有关命名的小问题。 在引理的陈述中,变量 x 是一个关于被替换的变量的隐式参数,同时在变量、抽象、 对自然数分项和不动点的赋型规则中,变量 x 是一个关于关联变量的隐式参数。 因此我们使用形同 {x = y} 的语法来将 y 与被替换的变量绑定, 用 {x = x} 来将 x⊢`⊢ƛ⊢case⊢μ 中关联变量绑定。 在这里用 y 这一名字也与前一章中替换的原始定义一致。 在证明中从来没有提及过 xyVN 的类型, 因此在下面的内容中,我们尽可能方便地选择类型名称。

解决了命名问题,接下来我们来分析前三种情况:

  • 当处于变量的情况时,我们必须证明:

    ∅ ⊢ V ⦂ B
    Γ , y ⦂ B ⊢ ` x ⦂ A
    ------------------------
    Γ ⊢ ` x [ y := V ] ⦂ A

    此时第二个假设形如:

    Γ , y ⦂ B ∋ x ⦂ A

    此处有两种子情况,取决于该命题的论据:

    • 查询判断由规则 Z 给出:

      ----------------
      Γ , x ⦂ A ∋ x ⦂ A

      在此情况下,xy 必须是一样的,同样 AB 也是。 尽管如此,我们必须要求值 x ≟ y 来化简替换的定义:

      • 如果变量相等,那么化简后我们必须证明:

        ∅ ⊢ V ⦂ A
        ---------
        Γ ⊢ V ⦂ A

        由弱化可得。

      • 如果变量不相等,我们则得到了一个矛盾。
    • 查询判断由规则 S 给出:

      x ≢ y
      Γ ∋ x ⦂ A
      -----------------
      Γ , y ⦂ B ∋ x ⦂ A

      在此情况下,xy 必须是不同的。 尽管如此,我们必须要求值 x ≟ y 来化简替换的定义:

      • 如果变量相等,我们则得到了一个矛盾。
      • 如果变量不相等,那么化简后我们必须证明:

        ∅ ⊢ V ⦂ B
        x ≢ y
        Γ ∋ x ⦂ A
        -------------
        Γ ⊢ ` x ⦂ A

        由变量的赋型规则可得。

  • 当处于抽象的情况时,我们必须证明:

    ∅ ⊢ V ⦂ B
    Γ , y ⦂ B ⊢ (ƛ x ⇒ N) ⦂ A ⇒ C
    --------------------------------
    Γ ⊢ (ƛ x ⇒ N) [ y := V ] ⦂ A ⇒ C

    其中第二条假设可由下得出:

    Γ , y ⦂ B , x ⦂ A ⊢ N ⦂ C

    我们求值 x ≟ y 来化简替换的定义:

    • 如果变量相等,那么化简后我们必须证明:

      ∅ ⊢ V ⦂ B
      Γ , x ⦂ B , x ⦂ A ⊢ N ⦂ C
      -------------------------
      Γ ⊢ ƛ x ⇒ N ⦂ A ⇒ C

      由去除引理可得:

      Γ , x ⦂ B , x ⦂ A ⊢ N ⦂ C
      -------------------------
      Γ , x ⦂ A ⊢ N ⦂ C

      抽象的赋型规则给出了所需的结论。

    • 如果变量不相等,那么化简后我们必须证明:

      ∅ ⊢ V ⦂ B
      x ≢ y
      Γ , y ⦂ B , x ⦂ A ⊢ N ⦂ C
      --------------------------------
      Γ ⊢ ƛ x ⇒ (N [ y := V ]) ⦂ A ⇒ C

      由交换引理可得:

      x ≢ y
      Γ , y ⦂ B , x ⦂ A ⊢ N ⦂ C
      -------------------------
      Γ , x ⦂ A , y ⦂ B ⊢ N ⦂ C

      归纳假设给出了:

      ∅ ⊢ V ⦂ B
      Γ , x ⦂ A , y ⦂ B ⊢ N ⦂ C
      ----------------------------
      Γ , x ⦂ A ⊢ N [ y := V ] ⦂ C

      抽象的赋型规则给出了所需的结论。

  • 当处于应用的情况时,我们必须证明:

    ∅ ⊢ V ⦂ C
    Γ , y ⦂ C ⊢ L · M ⦂ B
    --------------------------
    Γ ⊢ (L · M) [ y := V ] ⦂ B

    其中第二条假设可由下两条判断中得出:

    Γ , y ⦂ C ⊢ L ⦂ A ⇒ B
    Γ , y ⦂ C ⊢ M ⦂ A

    根据替换的定义,我们必须证明:

    ∅ ⊢ V ⦂ C
    Γ , y ⦂ C ⊢ L ⦂ A ⇒ B
    Γ , y ⦂ C ⊢ M ⦂ A
    ---------------------------------------
    Γ ⊢ (L [ y := V ]) · (M [ y := V ]) ⦂ B

    对于 LM 应用归纳引理,加上应用的赋型规则给出了所需的结论。

对剩下情况的证明大致相同,都是通过对各个子项做归纳。 当构造引入一个约束变量时我们需要将其与被替换的变量进行比较, 如果它们相同则应用去除引理,如果它们不同则应用交换引理。

对于 Agda 来说,我们写 x ≟ y 还是写 y ≟ x 是有区别的。 在交互式证明中,Agda 将显示 _[_:=_] 定义中的哪些剩余 with 子句需要简化, 而 subst 中的 with 子句需要精确匹配这些子句。 指导准则是 Agda 对于对称性和交换性一无所知,这需要调用适当的引理, 因此考虑参数的顺序和保持一致性非常重要。

练习 subst(延伸)

重写 subst 的定义,使得它与在上一章练习中修改过的 _[_:=_]′ 定义相兼容。 和之前一样,需要将处理约束变量的部分提取成一个单独的函数,与替换保持类型的 证明一同互递归定义。

-- 请将代码写在此处。

保型性

一旦我们证明了替换保持类型,证明规约保持类型是简单的:

preserve :  {M N A}
     M  A
   M —→ N
    ----------
     N  A
preserve (⊢` ())
preserve (⊢ƛ ⊢N)                 ()
preserve (⊢L · ⊢M)               (ξ-·₁ L—→L′)     =  (preserve ⊢L L—→L′) · ⊢M
preserve (⊢L · ⊢M)               (ξ-·₂ VL M—→M′)  =  ⊢L · (preserve ⊢M M—→M′)
preserve ((⊢ƛ ⊢N) · ⊢V)          (β-ƛ VV)         =  subst ⊢V ⊢N
preserve ⊢zero                   ()
preserve (⊢suc ⊢M)               (ξ-suc M—→M′)    =  ⊢suc (preserve ⊢M M—→M′)
preserve (⊢case ⊢L ⊢M ⊢N)        (ξ-case L—→L′)   =  ⊢case (preserve ⊢L L—→L′) ⊢M ⊢N
preserve (⊢case ⊢zero ⊢M ⊢N)     (β-zero)         =  ⊢M
preserve (⊢case (⊢suc ⊢V) ⊢M ⊢N) (β-suc VV)       =  subst ⊢V ⊢N
preserve (⊢μ ⊢M)                 (β-μ)            =  subst (⊢μ ⊢M) ⊢M

证明从来没有提到 MN 的类型, 因此在下面的内容中,我们尽可能方便地选择类型名称。

让我们分析规约规则的两种情况:

  • 规则 ξ-·₁。我们有

    L —→ L′
    ----------------
    L · M —→ L′ · M

    其中左手侧由

    Γ ⊢ L ⦂ A ⇒ B
    Γ ⊢ M ⦂ A
    -------------
    Γ ⊢ L · M ⦂ B

    赋型。

    根据归纳,我们有

    Γ ⊢ L ⦂ A ⇒ B
    L —→ L′
    --------------
    Γ ⊢ L′ ⦂ A ⇒ B

    其中右手侧的赋型可以直接得出。

  • 规则 β-ƛ。我们有

    Value V
    -----------------------------
    (ƛ x ⇒ N) · V —→ N [ x := V ]

    其中左手侧由

    Γ , x ⦂ A ⊢ N ⦂ B
    -------------------
    Γ ⊢ ƛ x ⇒ N ⦂ A ⇒ B    Γ ⊢ V ⦂ A
    --------------------------------
    Γ ⊢ (ƛ x ⇒ N) · V ⦂ B

    赋型。

    根据替换引理,我们有

    Γ ⊢ V ⦂ A
    Γ , x ⦂ A ⊢ N ⦂ B
    --------------------
    Γ ⊢ N [ x := V ] ⦂ B

    其中右手侧的赋型可以直接得出。

剩余情况与此类似,对每个 ξ 规则使用归纳, 对每个 β 规则使用替换引理。

求值

通过重复应用可进性和保型性,我们可以对任何良类型的项求值。 在这一节,我们将介绍一个 Agda 函数, 该函数可以求得任意给定良类型的闭项到其值的规约序列,如果该序列存在。

一些项将永远规约下去。这是一个例子:

sucμ  =  μ "x"  `suc (` "x")

_ =
  begin
    sucμ
  —→⟨ β-μ 
    `suc sucμ
  —→⟨ ξ-suc β-μ 
    `suc `suc sucμ
  —→⟨ ξ-suc (ξ-suc β-μ) 
    `suc `suc `suc sucμ
  --  ...
  

由于每个 Agda 计算都必须终止, 我们不能仅仅要求 Agda 将项规约为值。 相反,我们将向 Agda 提供一个自然数, 并允许它在需要比给定的数更多的规约步骤时终止规约。

加密货币也存在类似的问题。 使用智能合约的系统要求维护区块链的矿工评估体现合约的程序。 例如,验证以太坊上的交易可能需要为以太坊虚拟机(EVM)执行一个程序。 一个长期运行或非终止的程序可能会导致矿工在验证合同上投入任意多的努力, 但回报很少或没有回报。为了避免这种情况, 每笔交易都伴随着一定数量的燃料(Gas)可用于计算。 在 EVM 上执行的每个步骤都会收取公告数量的燃料, 交易以公布的费率支付燃料:每单位燃料支付给定数量的以太币(以太坊的货币)。

以此类推,我们将使用燃料作为参数的名称, 该参数限制了规约步骤的数量。Gas 由一个自然数指定。

record Gas : Set where
  constructor gas
  field
    amount : 

当我们的求值器返回了一个项 N,它要么证明 N 是一个值, 要么表明它耗尽了燃料:

data Finished (N : Term) : Set where

  done :
      Value N
      ----------
     Finished N

  out-of-gas :
      ----------
      Finished N

给定一个类型为 A 的项 L,对于某个 N, 求值器将返回从 LN 的规约序列以及规约是否完成的指示:

data Steps (L : Term) : Set where

  steps :  {N}
     L —↠ N
     Finished N
      ----------
     Steps L

求值器使用燃料和项是良类型的论据, 并返回相应的步骤:

eval :  {L A}
   Gas
     L  A
    ---------
   Steps L
eval {L} (gas zero)    ⊢L                     =  steps (L ) out-of-gas
eval {L} (gas (suc m)) ⊢L with progress ⊢L
... | done VL                                 =  steps (L ) (done VL)
... | step {M} L—→M with eval (gas m) (preserve ⊢L L—→M)
...    | steps M—↠N fin                       =  steps (L —→⟨ L—→M  M—↠N) fin

L 是我们要规约的项的名称,⊢L 是项 L 是良类型的论据。 我们考虑剩余的燃料量。此处有两种可能:

  • 如果是零,则我们过早地停止了。我们将返回简单的规约序列 L —↠ L, 并标明我们用尽了燃料。
  • 如果非零,则在下一个步骤中我们还剩下 m 燃料。将进度应用于项 L 是良类型的论据。 此处有两种可能:

    • L 是一个值,则我们已经完成了。 我们将返回简单的规约序列 L —↠ L,以及 L 是值的论据。
    • L 步进至另一个项 M。保型性提供了 M 也是良类型的论据, 我们对剩余的燃料递归调用 eval。 结果将得到 M —↠ N 的论据以及 N 是良类型的论据和规约是否完成的标识。 我们将 L —→ MM —↠ N 的论据结合来得到 L —↠ N 以及规约是否完成的标识。

例子

现在我们可以用 Agda 来计算之前给出的不停机的规约序列。 首先我们证明项 sucμ 是良赋型的:

⊢sucμ :   μ "x"  `suc ` "x"  `ℕ
⊢sucμ = ⊢μ (⊢suc (⊢` ∋x))
  where
  ∋x = Z

我们花三步量的燃料来进行求值, 以展示这个无穷规约序列的前三步:

_ : eval (gas 3) ⊢sucμ 
  steps
   (μ "x"  `suc ` "x"
   —→⟨ β-μ 
    `suc (μ "x"  `suc ` "x")
   —→⟨ ξ-suc β-μ 
    `suc (`suc (μ "x"  `suc ` "x"))
   —→⟨ ξ-suc (ξ-suc β-μ) 
    `suc (`suc (`suc (μ "x"  `suc ` "x")))
   )
   out-of-gas
_ = refl

类似地,我们可以用 Agda 计算前一章节中给出的规约序列。 我们从计算 Church 表示法表示数字的数字二应用到后继和数字零开始。 提供 100 步量的燃料就已远超过需求了:

_ : eval (gas 100) (⊢twoᶜ · ⊢sucᶜ · ⊢zero) 
  steps
   ((ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · (ƛ "n"  `suc ` "n")
   · `zero
   —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ "z"  (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · ` "z")) ·
     `zero
   —→⟨ β-ƛ V-zero 
    (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · `zero)
   —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    (ƛ "n"  `suc ` "n") · `suc `zero
   —→⟨ β-ƛ (V-suc V-zero) 
    `suc (`suc `zero)
   )
   (done (V-suc (V-suc V-zero)))
_ = refl

上面的例子是通过首先使用组合键 C-c C-n 来范式化等式的左侧, 然后将结果粘贴到等式右侧的方式生成的。前一章中规约的例子便是 通过使用这一方式导出结果、重新排版并且将展开的表达式对应地重 写为 twoᶜsucᶜ 的形式得到的。

接下来,我们来证明二加二的和是四:

_ : eval (gas 100) ⊢2+2 
  steps
   ((μ "+" 
     (ƛ "m" 
      (ƛ "n" 
       case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
       ])))
    · `suc (`suc `zero)
    · `suc (`suc `zero)
   —→⟨ ξ-·₁ (ξ-·₁ β-μ) 
    (ƛ "m" 
     (ƛ "n" 
      case ` "m" [zero⇒ ` "n" |suc "m" 
      `suc
      ((μ "+" 
        (ƛ "m" 
         (ƛ "n" 
          case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
          ])))
       · ` "m"
       · ` "n")
      ]))
    · `suc (`suc `zero)
    · `suc (`suc `zero)
   —→⟨ ξ-·₁ (β-ƛ (V-suc (V-suc V-zero))) 
    (ƛ "n" 
     case `suc (`suc `zero) [zero⇒ ` "n" |suc "m" 
     `suc
     ((μ "+" 
       (ƛ "m" 
        (ƛ "n" 
         case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
         ])))
      · ` "m"
      · ` "n")
     ])
    · `suc (`suc `zero)
   —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    case `suc (`suc `zero) [zero⇒ `suc (`suc `zero) |suc "m" 
    `suc
    ((μ "+" 
      (ƛ "m" 
       (ƛ "n" 
        case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
        ])))
     · ` "m"
     · `suc (`suc `zero))
    ]
   —→⟨ β-suc (V-suc V-zero) 
    `suc
    ((μ "+" 
      (ƛ "m" 
       (ƛ "n" 
        case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
        ])))
     · `suc `zero
     · `suc (`suc `zero))
   —→⟨ ξ-suc (ξ-·₁ (ξ-·₁ β-μ)) 
    `suc
    ((ƛ "m" 
      (ƛ "n" 
       case ` "m" [zero⇒ ` "n" |suc "m" 
       `suc
       ((μ "+" 
         (ƛ "m" 
          (ƛ "n" 
           case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
           ])))
        · ` "m"
        · ` "n")
       ]))
     · `suc `zero
     · `suc (`suc `zero))
   —→⟨ ξ-suc (ξ-·₁ (β-ƛ (V-suc V-zero))) 
    `suc
    ((ƛ "n" 
      case `suc `zero [zero⇒ ` "n" |suc "m" 
      `suc
      ((μ "+" 
        (ƛ "m" 
         (ƛ "n" 
          case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
          ])))
       · ` "m"
       · ` "n")
      ])
     · `suc (`suc `zero))
   —→⟨ ξ-suc (β-ƛ (V-suc (V-suc V-zero))) 
    `suc
    case `suc `zero [zero⇒ `suc (`suc `zero) |suc "m" 
    `suc
    ((μ "+" 
      (ƛ "m" 
       (ƛ "n" 
        case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
        ])))
     · ` "m"
     · `suc (`suc `zero))
    ]
   —→⟨ ξ-suc (β-suc V-zero) 
    `suc
    (`suc
     ((μ "+" 
       (ƛ "m" 
        (ƛ "n" 
         case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
         ])))
      · `zero
      · `suc (`suc `zero)))
   —→⟨ ξ-suc (ξ-suc (ξ-·₁ (ξ-·₁ β-μ))) 
    `suc
    (`suc
     ((ƛ "m" 
       (ƛ "n" 
        case ` "m" [zero⇒ ` "n" |suc "m" 
        `suc
        ((μ "+" 
          (ƛ "m" 
           (ƛ "n" 
            case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
            ])))
         · ` "m"
         · ` "n")
        ]))
      · `zero
      · `suc (`suc `zero)))
   —→⟨ ξ-suc (ξ-suc (ξ-·₁ (β-ƛ V-zero))) 
    `suc
    (`suc
     ((ƛ "n" 
       case `zero [zero⇒ ` "n" |suc "m" 
       `suc
       ((μ "+" 
         (ƛ "m" 
          (ƛ "n" 
           case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
           ])))
        · ` "m"
        · ` "n")
       ])
      · `suc (`suc `zero)))
   —→⟨ ξ-suc (ξ-suc (β-ƛ (V-suc (V-suc V-zero)))) 
    `suc
    (`suc
     case `zero [zero⇒ `suc (`suc `zero) |suc "m" 
     `suc
     ((μ "+" 
       (ƛ "m" 
        (ƛ "n" 
         case ` "m" [zero⇒ ` "n" |suc "m"  `suc (` "+" · ` "m" · ` "n")
         ])))
      · ` "m"
      · `suc (`suc `zero))
     ])
   —→⟨ ξ-suc (ξ-suc β-zero) 
    `suc (`suc (`suc (`suc `zero)))
   )
   (done (V-suc (V-suc (V-suc (V-suc V-zero)))))
_ = refl

再一次地,上一章中的推导是通过编辑上述内容得出的。

类似地,我们可以计算 Church 表示法表示的数字的相应项。

_ : eval (gas 100) ⊢2+2ᶜ 
  steps
   ((ƛ "m" 
     (ƛ "n" 
      (ƛ "s"  (ƛ "z"  ` "m" · ` "s" · (` "n" · ` "s" · ` "z")))))
    · (ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z")))
    · (ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z")))
    · (ƛ "n"  `suc ` "n")
    · `zero
   —→⟨ ξ-·₁ (ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ))) 
    (ƛ "n" 
     (ƛ "s" 
      (ƛ "z" 
       (ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · ` "s" ·
       (` "n" · ` "s" · ` "z"))))
    · (ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z")))
    · (ƛ "n"  `suc ` "n")
    · `zero
   —→⟨ ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ "s" 
     (ƛ "z" 
      (ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · ` "s" ·
      ((ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · ` "s" · ` "z")))
    · (ƛ "n"  `suc ` "n")
    · `zero
   —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ "z" 
     (ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · (ƛ "n"  `suc ` "n")
     ·
     ((ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · (ƛ "n"  `suc ` "n")
      · ` "z"))
    · `zero
   —→⟨ β-ƛ V-zero 
    (ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · (ƛ "n"  `suc ` "n")
    ·
    ((ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · (ƛ "n"  `suc ` "n")
     · `zero)
   —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ "z"  (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · ` "z")) ·
    ((ƛ "s"  (ƛ "z"  ` "s" · (` "s" · ` "z"))) · (ƛ "n"  `suc ` "n")
     · `zero)
   —→⟨ ξ-·₂ V-ƛ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ "z"  (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · ` "z")) ·
    ((ƛ "z"  (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · ` "z")) ·
     `zero)
   —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    (ƛ "z"  (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · ` "z")) ·
    ((ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · `zero))
   —→⟨ ξ-·₂ V-ƛ (ξ-·₂ V-ƛ (β-ƛ V-zero)) 
    (ƛ "z"  (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · ` "z")) ·
    ((ƛ "n"  `suc ` "n") · `suc `zero)
   —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc V-zero)) 
    (ƛ "z"  (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · ` "z")) ·
    `suc (`suc `zero)
   —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    (ƛ "n"  `suc ` "n") · ((ƛ "n"  `suc ` "n") · `suc (`suc `zero))
   —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc (V-suc V-zero))) 
    (ƛ "n"  `suc ` "n") · `suc (`suc (`suc `zero))
   —→⟨ β-ƛ (V-suc (V-suc (V-suc V-zero))) 
    `suc (`suc (`suc (`suc `zero)))
   )
   (done (V-suc (V-suc (V-suc (V-suc V-zero)))))
_ = refl

再一次地,上一节中的示例是通过编辑上述内容得出的。

练习 mul-eval(推荐)

用这个求值器来验证二乘二的积是四。

-- 请将代码写在此处。
练习 progress-preservation (实践)

不阅读上面的陈述, 写下简单类型 λ-演算可进性和保型性的定理。

-- 请将代码写在此处。
练习 subject_expansion (实践)

如果 M —→ N,我们便称 M 规约N, 相同的情形也被称作 N 扩展(Expand)M。 保型性有时也被叫做子规约(Subject Reduction)。 它的对应是子扩展(Subject Expansion), 如果 M —→ N∅ ⊢ N ⦂ A 蕴含 ∅ ⊢ M ⦂ A。 找到两个子扩展的反例,一个涉及 case 表达式而另一个不涉及。

-- 请将代码写在此处。

良类型的项不会卡住

一个项是范式,如果它不能被规约。

Normal : Term  Set
Normal M  =   {N}  ¬ (M —→ N)

一个项被卡住,如果它是一个范式但不是一个值。

Stuck : Term  Set
Stuck M  =  Normal M × ¬ Value M

使用可进性,很容易证明没有良类型的项会被卡住。

postulate
  unstuck :  {M A}
       M  A
      -----------
     ¬ (Stuck M)

使用保型性,很容易证明在经过任意多次步进后, 良类型的项依旧是良类型的。

postulate
  preserves :  {M N A}
       M  A
     M —↠ N
      ---------
       N  A

一个简单地结果是,从一个良类型的项开始,进行任意多次步进, 将得到一个不被卡住的项。

postulate
  wttdgs :  {M N A}
       M  A
     M —↠ N
      -----------
     ¬ (Stuck N)

Felleisen 与 Wright 通过可进性和保型性引入了证明,并将其总结为 良类型的项不会卡住 的口号。 (他们提及了 Robin Milner 早期的工作,他使用指称而非操作语义。 他引入了「错误」作为带有类型错误的术语的指称,并展示了 良类型的项不会出错。)

练习 stuck (实践)

给出一个会被卡住的不良类型的项的例子。

-- 请将代码写在此处。
练习 unstuck (推荐)

提供上文中 unstuckpreserveswttdgs 三个假设的证明。

-- 请将代码写在此处。

规约是确定的

当我们引入归约时,我们声称它是确定的。 为完整起见,我们在此提供正式的证明。

我们的证明需要一个合同变体来处理四个参数的函数(处理case_[zero⇒_|suc_⇒_])。 它与之前定义的 congcong₂ 完全类似:

cong₄ :  {A B C D E : Set} (f : A  B  C  D  E)
  {s w : A} {t x : B} {u y : C} {v z : D}
   s  w  t  x  u  y  v  z  f s t u v  f w x y z
cong₄ f refl refl refl refl = refl

现在证明规约是确定的十分简单。

det :  {M M′ M″}
   (M —→ M′)
   (M —→ M″)
    --------
   M′  M″
det (ξ-·₁ L—→L′)   (ξ-·₁ L—→L″)     =  cong₂ _·_ (det L—→L′ L—→L″) refl
det (ξ-·₁ L—→L′)   (ξ-·₂ VL M—→M″)  =  ⊥-elim (V¬—→ VL L—→L′)
det (ξ-·₁ L—→L′)   (β-ƛ _)          =  ⊥-elim (V¬—→ V-ƛ L—→L′)
det (ξ-·₂ VL _)    (ξ-·₁ L—→L″)     =  ⊥-elim (V¬—→ VL L—→L″)
det (ξ-·₂ _ M—→M′) (ξ-·₂ _ M—→M″)   =  cong₂ _·_ refl (det M—→M′ M—→M″)
det (ξ-·₂ _ M—→M′) (β-ƛ VM)         =  ⊥-elim (V¬—→ VM M—→M′)
det (β-ƛ _)        (ξ-·₁ L—→L″)     =  ⊥-elim (V¬—→ V-ƛ L—→L″)
det (β-ƛ VM)       (ξ-·₂ _ M—→M″)   =  ⊥-elim (V¬—→ VM M—→M″)
det (β-ƛ _)        (β-ƛ _)          =  refl
det (ξ-suc M—→M′)  (ξ-suc M—→M″)    =  cong `suc_ (det M—→M′ M—→M″)
det (ξ-case L—→L′) (ξ-case L—→L″)   =  cong₄ case_[zero⇒_|suc_⇒_]
                                         (det L—→L′ L—→L″) refl refl refl
det (ξ-case L—→L′) β-zero           =  ⊥-elim (V¬—→ V-zero L—→L′)
det (ξ-case L—→L′) (β-suc VL)       =  ⊥-elim (V¬—→ (V-suc VL) L—→L′)
det β-zero         (ξ-case M—→M″)   =  ⊥-elim (V¬—→ V-zero M—→M″)
det β-zero         β-zero           =  refl
det (β-suc VL)     (ξ-case L—→L″)   =  ⊥-elim (V¬—→ (V-suc VL) L—→L″)
det (β-suc _)      (β-suc _)        =  refl
det β-μ            β-μ              =  refl

证明通过对可能的规约进行归纳来完成。我们考虑三种典型的情况:

  • 两个关于 ξ-·₁ 的实例:

    L —→ L′                 L —→ L″
    --------------- ξ-·₁    --------------- ξ-·₁
    L · M —→ L′ · M         L · M —→ L″ · M

    根据归纳我们有 L′ ≡ L″,因此根据合同性有 L′ · M ≡ L″ · M

  • 一个关于 ξ-·₁ 的实例和一个关于 ξ-·₂ 的实例:

                            Value L
    L —→ L′                 M —→ M″
    --------------- ξ-·₁    --------------- ξ-·₂
    L · M —→ L′ · M         L · M —→ L · M″

    左侧的规则要求 L 被规约,但右侧的规则要求 L 是一个值。 这是一个矛盾,因为值无法被规约。如果值的约束从 ξ-·₂ 或任何其他规约规则中被移除, 那么确定性将不再适用。

  • 两个关于 β-ƛ 的实例:

    Value V                              Value V
    ----------------------------- β-ƛ    ----------------------------- β-ƛ
    (ƛ x ⇒ N) · V —→ N [ x := V ]        (ƛ x ⇒ N) · V —→ N [ x := V ]

    因为左侧是相同的,所以右侧也是相同的。 形式证明只是对 refl 的调用。

上述证明中的 18 行中有 5 行是多余的, 例如,当一个规则是 ξ-·₁ 而另一个是 ξ-·₂ 的情况被考虑两次, 一次是先有 ξ-·₁,然后有 ξ-·₂,另一次将两者互换。 我们可能想做的是删除多余的行并在证明的底部添加

det M—→M′ M—→M″ = sym (det M—→M″ M—→M′)

但这不起作用:停机检查器报错,因为参数只是被交换了顺序,而且没有任何一个变得更小。

小测验

假设我们加入了一个新项 zap 以及以下规约规则

-------- β-zap
M —→ zap

和以下赋型规则:

----------- ⊢zap
Γ ⊢ zap ⦂ A

在这些规则存在的情况下,以下哪些属性仍然成立? 对于每个属性,写下 「仍为真」 或 「变为假」。 如果一个属性变为假,请举出一个反例:

  • 确定性

  • 可进性

  • 保型性

小测验

假设我们加入了一个新项 foo 以及以下规约规则:

------------------ β-foo₁
(λ x ⇒ ` x) —→ foo

----------- β-foo₂
foo —→ zero

在此规则存在的情况下,以下哪些属性仍然成立? 对于每个属性,写下 「仍为真」 或 「变为假」。 如果一个属性变为假,请举出一个反例:

  • 确定性

  • 可进性

  • 保型性

小测验

假设我们从步进关系中移除了规则 ξ·₁。 在此规则不存在的情况下,以下哪些属性仍然成立? 对于每个属性,写下 「仍为真」 或 「变为假」。 如果一个属性变为假,请举出一个反例:

  • 确定性

  • 可进性

  • 保型性

小测验

我们可以通过按照字典序写出所有类型为 `ℕ ⇒ `ℕ 的程序来遍历所有从自然数到自然数的可计算函数。 将这个列表中的第 i 个函数写作 fᵢ

假设我们添加了一个赋性规则,应用上述遍历来将一个自然数解释为一个从自然数到自然数的函数:

Γ ⊢ L ⦂ `ℕ
Γ ⊢ M ⦂ `ℕ
-------------- _·ℕ_
Γ ⊢ L · M ⦂ `ℕ

并且我们添加了相应的归约规则:

fᵢ(m) —→ n
---------- δ
i · m —→ n

在此规则存在的情况下,以下哪些属性仍然成立? 对于每个属性,写下 「仍为真」 或 「变为假」。 如果一个属性变为假,请举出一个反例:

  • 确定性

  • 可进性

  • 保型性

在这种情况下是否保留了所有属性? 我们是否希望对系统进行任何其他更改?

Unicode

本章使用了下列 Unicode:

ƛ  U+019B  LATIN SMALL LETTER LAMBDA WITH STROKE (\Gl-)
Δ  U+0394  GREEK CAPITAL LETTER DELTA (\GD or \Delta)
β  U+03B2  GREEK SMALL LETTER BETA (\Gb or \beta)
δ  U+03B4  GREEK SMALL LETTER DELTA (\Gd or \delta)
μ  U+03BC  GREEK SMALL LETTER MU (\Gm or \mu)
ξ  U+03BE  GREEK SMALL LETTER XI (\Gx or \xi)
ρ  U+03B4  GREEK SMALL LETTER RHO (\Gr or \rho)
ᵢ  U+1D62  LATIN SUBSCRIPT SMALL LETTER I (\_i)
ᶜ  U+1D9C  MODIFIER LETTER SMALL C (\^c)
–  U+2013  EM DASH (\em)
₄  U+2084  SUBSCRIPT FOUR (\_4)
↠  U+21A0  RIGHTWARDS TWO HEADED ARROW (\rr-)
⇒  U+21D2  RIGHTWARDS DOUBLE ARROW (\=>)
∅  U+2205  EMPTY SET (\0)
∋  U+220B  CONTAINS AS MEMBER (\ni)
≟  U+225F  QUESTIONED EQUAL TO (\?=)
⊢  U+22A2  RIGHT TACK (\vdash or \|-)
⦂  U+2982  Z NOTATION TYPE COLON (\:)

DeBruijn: 内在类型的 de Bruijn 表示法

module plfa.part2.DeBruijn where

前面两个章节介绍了 λ-演算,用以带名字的变量进行形式化,而且将项与类型分开定义。 我们之所以使用这样的方法,是因为这是传统的定义方法,但不是我们推荐的方法。 在本节中,我们使用另一种方法,用 de Bruijn 因子来代替带名字的变量,并且用项的类型来索引项。 这种新的表示法更加紧凑,可以使用更少的代码来证明相同的内容。

表示带类型的 λ-演算有两种基本的方法。 其一是我们在前两章中使用的方法,先定义项,再定义类型。 项独立于类型存在,其类型由另外的赋型规则指派。 其二是我们在本章中使用的方法,先定义类型,再定义项。 项和类型的规则相互环绕,并且讨论不带类型的项将没有意义。 这两种方法有的时候被称为柯里法(Curry Style)邱奇法(Church Style)。 沿用 Reynolds 的叫法,我们把两种方法称为外在法(Extrinsic)内在法(Intrinsic)

我们在这里使用的这种表示法最先由 Thorsten Altenkirch 和 Bernhard Reus 提出。 使用的将重命名和替换形式化的方法由 Conor McBride 提出。 James Chapman、James McKinna 和许多其他人也进行了相关的研究。

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl)
open import Data.Empty using (; ⊥-elim)
open import Data.Nat using (; zero; suc; _<_; _≤?_; z≤n; s≤s)
open import Relation.Nullary using (¬_)
open import Relation.Nullary.Decidable using (True; toWitness)

简介

项的结构和其良类型的推导的结构联系很紧密。例如,这里是 Church 法表示的二:

twoᶜ : Term
twoᶜ = ƛ "s" ⇒ ƛ "z" ⇒ ` "s" · (` "s" · ` "z")

这里是它对应的赋型推导:

⊢twoᶜ : ∀ {A} → ∅ ⊢ twoᶜ ⦂ Ch A
⊢twoᶜ = ⊢ƛ (⊢ƛ (⊢` ∋s · (⊢` ∋s · ⊢` ∋z)))
  where
  ∋s = S′ Z
  ∋z = Z

(两者都摘自 Lambda 章节,你可以在这里查看完整的推导树。) 两者的定义对应的很紧密,其中:

  • `_ 对应了 ⊢`
  • ƛ_⇒_ 对应了 ⊢ƛ
  • _·_ 对应了 _·_

此外,如果我们将 Z 看作零,将 S 看做后继,那么每个变量的查询推导对应了 一个自然数:它告诉我们到达此变量的约束之前,经过了多少约束项。 这里的 "z" 对应了 Z 或者零,"s" 对应了 S Z 或者一。 的确,"z" 被里面的抽象约束(向外跨过 0 个抽象), "s" 被外面的抽象约束(向外跨过 1 个抽象)。

本章中,我们利用这个对应特性,引入一种新的项的记法,其同时表示了项以及它的类型推导。 现在,我们写出如下:

twoᶜ  :  ∅ ⊢ Ch `ℕ
twoᶜ  =  ƛ ƛ (# 1 · (# 1 · # 0))

变量由一个自然数表示(用 ZS 表示,简写为常见形式),它告诉我们 这个变量的约束在多少个约束之外。因此,# 0 由里面的 ƛ 约束,# 1 由外面的 ƛ 约束。

用数字代替变量的这种表示方法叫做 de Bruijn 表示法,这些数字本身被称为 de Bruijn 因子(de Bruijn Indices),得名于荷兰数学家 Nicolaas Govert (Dick) de Bruijn (1918 - 2012),一位创造证明助理的先锋。 使用 de Bruijn 因子表示变量的一个好处是:每个项有一个唯一的表示方法,而不是 在 α-重命名下的一个相等类。

我们选择的表示方式的另一个重要特性是:他是内在类型(Intrinsically Typed)的。 在前两章中,项和类型的定义是完全分离的。所有的项拥有 Term 类型,Agda 并不会 阻止我们写出例如 `zero · `suc `zero 的没有类型的无意义的项。 这样独立于类型存在的项有时被称为原项(Preterms)或者源项(Raw Terms)。 我们将用 Γ ⊢ A 类型的内在类型的项,表示它在 Γ 语境中拥有类型 A, 来取代 Term 类型的源项。

尽管这两个选择很适合我们,这两个选择仍然是独立的。 可以用 de Bruijn 因子配合源项,也可以用内在类型的项配合变量名。 在 Untyped 章节中,我们将使用 de Bruijn 表示内在作用域的项, 但不包含类型。

第二个例子

De Bruijn 因子可能掌握起来有点棘手。在我们继续之前,我们先来考虑第二个例子。 下面是一个将两个自然数相加的一个项:

plus : Term
plus = μ "+" ⇒ ƛ "m" ⇒ ƛ "n" ⇒
         case ` "m"
           [zero⇒ ` "n"
           |suc "m" ⇒ `suc (` "+" · ` "m" · ` "n") ]

注意变量 "m" 被约束了两次,一次在 λ 抽象中,另一次在匹配表达式的后继分支中。 由于屏蔽效应,在匹配表达式后继分支出现的 "m" 必须指代后面的约束。

下面是它对应的类型推导:

⊢plus : ∅ ⊢ plus ⦂ `ℕ ⇒ `ℕ ⇒ `ℕ
⊢plus = ⊢μ (⊢ƛ (⊢ƛ (⊢case (⊢` ∋m) (⊢` ∋n)
         (⊢suc (⊢` ∋+ · ⊢` ∋m′ · ⊢` ∋n′)))))
  where
  ∋+  = (S′ (S′ (S′ Z)))
  ∋m  = (S′ Z)
  ∋n  = Z
  ∋m′ = Z
  ∋n′ = (S′ Z)

两者的定义对应的很紧密,除去之前的对应,我们注意到:

  • `zero 对应了 ⊢zero
  • `suc_ 对应了 ⊢suc
  • case_[zero⇒_|suc_⇒_] 对应了 ⊢case
  • μ_⇒_ 对应了 ⊢μ

注意到,查询判断 ∋m∋m′ 表示了两个名称同为 "m" 变量的不同约束。 作为对比, 判断 ∋n∋n′ 都表示变量 "n" 的约束,但是其语境不同, 前者中 "n" 是语境中最后一个约束,后者中则是在匹配表达式后继分支中 "m" 约束之后。

下面是用本章中的记法表示的这个项极其类型推导:

plus : ∀ {Γ} → Γ ⊢ `ℕ ⇒ `ℕ ⇒ `ℕ
plus = μ ƛ ƛ case (# 1) (# 0) (`suc (# 3 · # 0 · # 1))

从左往右,每个 de Bruijn 因子对应了一个查询判断:

  • # 1 对应了 ∋m
  • # 0 对应了 ∋n
  • # 3 对应了 ∋+
  • # 0 对应了 ∋m′
  • # 1 对应了 ∋n′

De Bruijn 因子计算了对应查询推断中 S 构造子的数量。 里面抽象约束的变量 "n" 在匹配表达式零分支中由 # 0 表示, 但在后继分支中由 # 1 表示,因为中间产生了约束。 抽象约束的变量 "m" 第一次出现是由 # 1 表示,然而第二次出现在匹配表达式 的后继分支时则由 # 0 表示。这里没有屏蔽效应————使用变量名时,我们无法 在后者的作用域内指代外部的约束,但是我们可以用 de Bruijn 因子 # 2 来指代。

展示的顺序

在本章中,使用内在类型的项要求我们在引入诸如重命名或者替换等操作的同时,证明它们保留了类型。 因此,我们必须改变展示的顺序。

项的语法现在包含了它们的赋型规则。 替换的定义现在更加深入,但包括了之前证明中最棘手的部分,即替换保留了类型。 规约的定义现在包括了保型性,不需要额外的证明。

语法

现在,我们开始正式的定义。

我们首先定义中缀声明。我们分别列出判断、类型和项的运算符:

infix  4 _⊢_
infix  4 _∋_
infixl 5 _,_

infixr 7 _⇒_

infix  5 ƛ_
infix  5 μ_
infixl 7 _·_
infix  8 `suc_
infix  9 `_
infix  9 S_
infix  9 #_

由于项是内在类型的,我们必须在定义项之前先定义类型和语境。

类型

与以前一样,我们只有两种类型:函数和自然数。它的形式化定义没有变化:

data Type : Set where
  _⇒_ : Type  Type  Type
  `ℕ  : Type

语境如同之前一样,但是我们舍去了名字。 语境如下形式化:

data Context : Set where
     : Context
  _,_ : Context  Type  Context

语境就是一个类型的列表,其最近约束的变量出现在右边。 如之前一样,我们使用 ΓΔ 来表示语境。 我们用 表示空语境,用 Γ , A 表示以 A 扩充的语境 Γ。 例如:

_ : Context
_ =  , `ℕ  `ℕ , `ℕ

在作用域中有两个变量,外部约束的变量的类型是 `ℕ ⇒ `ℕ,内部约束的类型是 `ℕ

变量及查询判断

内在类型的变量对应着查询判断。它们由 de Bruijn 因子表示,因此也对应着自然数。 我们用

Γ ∋ A

表示语境 Γ 中带有类型 A 的变量。 查询判断由一个以语境和类型索引的数据类型来形式化。 它和之前的查询判断看上去一样,但是变量名被舍去了:

data _∋_ : Context  Type  Set where

  Z :  {Γ A}
      ---------
     Γ , A  A

  S_ :  {Γ A B}
     Γ  A
      ---------
     Γ , B  A

S 构造子不再需要额外的参数,由于没有名字以后就不需要处理屏蔽效应。 现在的构造子 ZS 更紧密地对应了列表中成员关系的构造子 herethere, 以及自然数的构造子 zerosuc

例如,我们考虑下面的旧式查询判断:

  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ∋ "z" ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ∋ "s" ⦂ `ℕ ⇒ `ℕ

它们对应了下列内在类型的变量:

_ :  , `ℕ  `ℕ , `ℕ  `ℕ
_ = Z

_ :  , `ℕ  `ℕ , `ℕ  `ℕ  `ℕ
_ = S Z

在给出的语境中,"z"Z 表示(最近约束的变量), "s"S Z 表示(下一个最近约束的变量)。

项以及赋型判断

内在类型的项对应了其赋型判断。我们用

Γ ⊢ A

表示在语境 Γ 中类型为 A 的项。 这个判断用由语境和类型索引的数据类型进行形式化。 它和之前的查询判断看上去一样,但是变量名被舍去了:

data _⊢_ : Context  Type  Set where

  `_ :  {Γ A}
     Γ  A
      -----
     Γ  A

  ƛ_  :  {Γ A B}
     Γ , A  B
      ---------
     Γ  A  B

  _·_ :  {Γ A B}
     Γ  A  B
     Γ  A
      ---------
     Γ  B

  `zero :  {Γ}
      ---------
     Γ  `ℕ

  `suc_ :  {Γ}
     Γ  `ℕ
      ------
     Γ  `ℕ

  case :  {Γ A}
     Γ  `ℕ
     Γ  A
     Γ , `ℕ  A
      ----------
     Γ  A

  μ_ :  {Γ A}
     Γ , A  A
      ---------
     Γ  A

这个定义利用了项的结构和其良类型推导的结构之间紧密的对应关系:我们现在 使用推导当作项。

例如,考虑下列旧式的赋型判断:

  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "z" ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "s" ⦂ `ℕ ⇒ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "s" · ` "z" ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ , "z" ⦂ `ℕ ⊢ ` "s" · (` "s" · ` "z") ⦂ `ℕ
  • ∅ , "s" ⦂ `ℕ ⇒ `ℕ ⊢ (ƛ "z" ⇒ ` "s" · (` "s" · ` "z")) ⦂ `ℕ ⇒ `ℕ
  • ∅ ⊢ ƛ "s" ⇒ ƛ "z" ⇒ ` "s" · (` "s" · ` "z")) ⦂ (`ℕ ⇒ `ℕ) ⇒ `ℕ ⇒ `ℕ

它们对应了下列内在类型的项:

_ :  , `ℕ  `ℕ , `ℕ  `ℕ
_ = ` Z

_ :  , `ℕ  `ℕ , `ℕ  `ℕ  `ℕ
_ = ` S Z

_ :  , `ℕ  `ℕ , `ℕ  `ℕ
_ = ` S Z · ` Z

_ :  , `ℕ  `ℕ , `ℕ  `ℕ
_ = ` S Z · (` S Z · ` Z)

_ :  , `ℕ  `ℕ  `ℕ  `ℕ
_ = ƛ (` S Z · (` S Z · ` Z))

_ :   (`ℕ  `ℕ)  `ℕ  `ℕ
_ = ƛ ƛ (` S Z · (` S Z · ` Z))

最后的项表示了 Church 法表示的二。

简化 de Bruijn 因子

我们定义一个辅助函数来计算语境的长度,它会在之后确保一个因子在语境约束中有帮助:

length : Context  
length         =  zero
length (Γ , _)  =  suc (length Γ)

我们可以用一个自然数来从语境中选择一个类型:

lookup : {Γ : Context}  {n : }  (p : n < length Γ)  Type
lookup {(_ , A)} {zero}    (s≤s z≤n)  =  A
lookup {(Γ , _)} {(suc n)} (s≤s p)    =  lookup p

我们希望只在自然数小于语境长度的时候应用这个函数,由 p 来印证。

结合上述,我们可以将一个自然数转换成其对应的 de Bruijn 因子,从语境中查询它的类型:

count :  {Γ}  {n : }  (p : n < length Γ)  Γ  lookup p
count {_ , _} {zero}    (s≤s z≤n)  =  Z
count {Γ , _} {(suc n)} (s≤s p)    =  S (count p)

然后,我们可以引入一个变量的简略表示方法:

#_ :  {Γ}
   (n : )
   {n∈Γ : True (suc n ≤? length Γ)}
    --------------------------------
   Γ  lookup (toWitness n∈Γ)
#_ n {n∈Γ}  =  ` count (toWitness n∈Γ)

函数 #_ 取一个隐式参数 n∈Γ 来证明 n 在语境的约束内。 回忆 True_≤?_toWitnessDecidable 章节中定义。 n∈Γ 的类型保证了 #_ 不会在 n 超出语境约束时被应用。 最后,在返回类型中,n∈Γ 被转换为 n 在语境约束内的证明。

使用这种缩略方法,我们可以用更简洁的方法写出 Church 法表示的二:

_ :   (`ℕ  `ℕ)  `ℕ  `ℕ
_ = ƛ ƛ (# 1 · (# 1 · # 0))

测试例子

我们重复 Lambda 中的测试例子。 你可以在这里找到它们,并加以对比。

首先计算自然数二加二:

two :  {Γ}  Γ  `ℕ
two = `suc `suc `zero

plus :  {Γ}  Γ  `ℕ  `ℕ  `ℕ
plus = μ ƛ ƛ (case (# 1) (# 0) (`suc (# 3 · # 0 · # 1)))

2+2 :  {Γ}  Γ  `ℕ
2+2 = plus · two · two

我们推广到任意语境,因为我们在稍后给出 two 在内部约束时出现的例子。

接下来,计算 Church 法表示的二加二:

Ch : Type  Type
Ch A  =  (A  A)  A  A

twoᶜ :  {Γ A}  Γ  Ch A
twoᶜ = ƛ ƛ (# 1 · (# 1 · # 0))

plusᶜ :  {Γ A}  Γ  Ch A  Ch A  Ch A
plusᶜ = ƛ ƛ ƛ ƛ (# 3 · # 1 · (# 2 · # 1 · # 0))

sucᶜ :  {Γ}  Γ  `ℕ  `ℕ
sucᶜ = ƛ `suc (# 0)

2+2ᶜ :  {Γ}  Γ  `ℕ
2+2ᶜ = plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero

如同之前那样,我们推广到任意语境。 同时,我们将 twoᶜplusᶜ 推广至任意类型的 Church 法表示的自然数。

练习 mul (推荐)

用内在类型和 de Bruijn 法写出一个将两个自然数相乘的项。

-- 请将代码写在此处。

重命名

重命名是替换之前重要的一步,让我们可以将一个项从一个语境中 「转移」 至另一个语境。 它直接对应了上一章中的关于重命名的结论,但此处重命名保留类型的定理同时是进行重命名的代码。

和之前一样,我们首先需要一条扩充引理,使我们可以到遇到约束时扩充我们的语境。 给定一个将一个语境中的变量映射至另一个语境中变量的映射,扩充会生成一个 从扩充后的第一个语境至以相同方法扩充的第二个语境的映射。 它看上去和之前的扩充引理完全一样,只是舍去了名字和项:

ext :  {Γ Δ}
   (∀ {A}        Γ  A      Δ  A)
    ---------------------------------
   (∀ {A B}  Γ , B  A  Δ , B  A)
ext ρ Z      =  Z
ext ρ (S x)  =  S (ρ x)

ρ 为从 Γ 中变量至 Δ 中变量的映射的名称。 考虑 Γ , B 中变量的 de Bruijn 因子:

  • 如果它是 Z,在 Γ , B 中的类型是 B,那么返回 Z,在 Δ , B 中的类型也是 B

  • 如果它是 S x,其中 x 是某个 Γ 中的变量,那么 ρ xΔ 中的一个变量, 因此 S (ρ x) 是一个 Δ , B 中的变量。

定义了扩充过后,重命名的定义就变得很直接了。 如果一个语境中的变量映射至另一个语境中的变量,那么第一个语境 中的项映射至第二个语境中:

rename :  {Γ Δ}
   (∀ {A}  Γ  A  Δ  A)
    -----------------------
   (∀ {A}  Γ  A  Δ  A)
rename ρ (` x)          =  ` (ρ x)
rename ρ (ƛ N)          =  ƛ (rename (ext ρ) N)
rename ρ (L · M)        =  (rename ρ L) · (rename ρ M)
rename ρ (`zero)        =  `zero
rename ρ (`suc M)       =  `suc (rename ρ M)
rename ρ (case L M N)   =  case (rename ρ L) (rename ρ M) (rename (ext ρ) N)
rename ρ (μ N)          =  μ (rename (ext ρ) N)

ρ 为从 Γ 中变量至 Δ 中变量的映射的名称。 我们首先来解释前三种情况:

  • 如果项是一个变量,直接应用 ρ

  • 如果项是一个抽象,使用之前的扩充结论来扩充映射 ρ, 然后在递归地对于抽象本体进行重命名。

  • 如果项是一个应用,递归地重命名函数及其参数。

剩下的情况都很类似,在各个项中递归,并在引入约束变量的时候扩充映射。

之前,重命名是一个将项在一个语境中良类型的证明转换成项在另一个语境中 良类型的结论;而现在,它直接转换了整个项,调整了其中的约束变了。 在 Agda 中类型检查这段代码保证了只有在简单类型的 λ-演算中良类型的项 可以作为参数或者作为返回项。

下面的例子将带有一个自由变量,一个约束变量的项进行重命名:

M₀ :  , `ℕ  `ℕ  `ℕ  `ℕ
M₀ = ƛ (# 1 · (# 1 · # 0))

M₁ :  , `ℕ  `ℕ , `ℕ  `ℕ  `ℕ
M₁ = ƛ (# 2 · (# 2 · # 0))

_ : rename S_ M₀  M₁
_ = refl

通常来说,rename S_ 会把所有自由变量的 de Bruijn 因子增加一, 而不改变约束变量的 de Bruijn 因子。 这个代码自然地完成了这样的操作:映射将所有的变量增加一,然而在扩充作用下, 每个约束变量的因子不变。

我们稍后可以看到,使用 S_ 进行重命名在替换中起到了重要作用。 对于没有使用内在类型的 de Bruijn 因子表示法来说,这会有一点棘手。 这种方法需要记忆一个因子数,更大的因子为自由变量,更小的因子为约束变量。 这样很容易出现差一错误,而出现差一错误以后很难保证类型的保存性。 因此这里内在类型的 de Bruijn 项的 Agda 代码是本质上更加可靠。

同时替换

由于 de Bruijn 因子让我们免去了重命名的顾虑,给出一个更广义的替换的定义更加方便。 与其用一个闭项来替换一个单一的变量,广义的替换提供一个将原来项中各个自由变量至另一个项的映射。 除此之外,被替换的项可以在任意语境之中,不需要为闭项。

定义和证明的结构与重命名项类似。 同样,我们首先需要一个扩充引理,让我们能够在遇到约束时扩充语境。 对比在重命名中我们使用从一个语境中的变量至另一语境中变量的映射, 替换使用的是将一个语境中的变量至另一语境中的映射。 给定一个将一个语境中的变量映射至另一个语境中项的映射,扩充会生成一个 从扩充后的第一个语境至以相同方法扩充的第二个语境的映射。

exts :  {Γ Δ}
   (∀ {A}        Γ  A      Δ  A)
    ---------------------------------
   (∀ {A B}  Γ , B  A  Δ , B  A)
exts σ Z      =  ` Z
exts σ (S x)  =  rename S_ (σ x)

σ 为从 Γ 中变量至 Δ 中变量的项的名称。 考虑 Γ , B 中变量的 de Bruijn 因子:

  • 如果它是 Z,在 Γ , B 中的类型是 B, 那么返回 ` Z 项,在 Δ , B 中的类型也是 B

  • 如果它是 S x,其中 x 是某个 Γ 中的变量,那么 σ xΔ 中的一个项, 因此 S_ (σ x) 是一个 Δ , B 中的项。

这也是为什么我们需要先定义重命名,因为我们需要这个定义来将 语境 Δ 中的项重命名至扩充后的语境 Δ , B

定义了扩充过后,替换的定义就变得很直接了。 如果一个语境中的变量映射至另一个语境中的项,那么第一个语境 中的项映射至第二个语境中:

subst :  {Γ Δ}
   (∀ {A}  Γ  A  Δ  A)
    -----------------------
   (∀ {A}  Γ  A  Δ  A)
subst σ (` x)          =  σ x
subst σ (ƛ N)          =  ƛ (subst (exts σ) N)
subst σ (L · M)        =  (subst σ L) · (subst σ M)
subst σ (`zero)        =  `zero
subst σ (`suc M)       =  `suc (subst σ M)
subst σ (case L M N)   =  case (subst σ L) (subst σ M) (subst (exts σ) N)
subst σ (μ N)          =  μ (subst (exts σ) N)

σ 为从 Γ 中变量至 Δ 中项的映射的名称。 我们首先来解释前三种情况:

  • 如果项是一个变量,直接应用 σ

  • 如果项是一个抽象,使用之前的扩充结论来扩充映射 σ, 然后在递归地对于抽象本体进行进行替换。

  • 如果项是一个应用,递归地替换函数及其参数。

剩下的情况都很类似,在各个项中递归,并在引入约束变量的时候扩充映射。

单个替换

从广义的替换多个自由变量,我们可以定义只替换单个变量的特殊情况:

_[_] :  {Γ A B}
   Γ , B  A
   Γ  B
    ---------
   Γ  A
_[_] {Γ} {A} {B} N M =  subst {Γ , B} {Γ} σ {A} N
  where
  σ :  {A}  Γ , B  A  Γ  A
  σ Z      =  M
  σ (S x)  =  ` x

在一个语境 Γ , B 中类型为 A 的项,我们将类型为 B 的变量 替换为语境 Γ 中一个类型为 B 的项。 为了这么做,我们用一个从 Γ , B 语境至 Γ 语境的映射, 令语境中最后一个变量映射至类型为 B 的替换项,令任何其他自由变量映射至其本身。

考虑之前的例子:

  • (ƛ "z" ⇒ ` "s" · (` "s" · ` "z")) [ "s" := sucᶜ ]ƛ "z" ⇒ sucᶜ · (sucᶜ · ` "z")

下面是形式化后的例子:

M₂ :  , `ℕ  `ℕ  `ℕ  `ℕ
M₂ = ƛ # 1 · (# 1 · # 0)

M₃ :   `ℕ  `ℕ
M₃ = ƛ `suc # 0

M₄ :   `ℕ  `ℕ
M₄ = ƛ (ƛ `suc # 0) · ((ƛ `suc # 0) · # 0)

_ : M₂ [ M₃ ]  M₄
_ = refl

之前,我们展示了一个我们没有实现的替换的例子,因为它需要将约束变量重命名来防止捕捉:

  • (ƛ "x" ⇒ ` "x" · ` "y") [ "y" := ` "x" · `zero ] 应当得 ƛ "z" ⇒ ` "z" · (` "x" · `zero)

假设约束的 "x" 的类型是 `ℕ ⇒ `ℕ,被替换的 "y" 的类型是 `ℕ, 自由的 "x" 的类型也是 `ℕ ⇒ `ℕ。 下面是形式化后的例子:

M₅ :  , `ℕ  `ℕ , `ℕ  (`ℕ  `ℕ)  `ℕ
M₅ = ƛ # 0 · # 1

M₆ :  , `ℕ  `ℕ  `ℕ
M₆ = # 0 · `zero

M₇ :  , `ℕ  `ℕ  (`ℕ  `ℕ)  `ℕ
M₇ = ƛ (# 0 · (# 1 · `zero))

_ : M₅ [ M₆ ]  M₇
_ = refl

逻辑学家 Haskell Curry 注意到,正确地定义替换可能很棘手。 使用 de Bruijn 因子来定义替换可能更加棘手,且不易理解。 在现在的方法中,任何替换的定义必须保存类型。 虽然这样让定义更加深入,但这也意味着一旦定义完成以后最难的部分就完成了。 将定义和证明结合在一起可以让错误更不易出现。

值的定义与之前差不多:

data Value :  {Γ A}  Γ  A  Set where

  V-ƛ :  {Γ A B} {N : Γ , A  B}
      ---------------------------
     Value (ƛ N)

  V-zero :  {Γ}
      -----------------
     Value (`zero {Γ})

  V-suc :  {Γ} {V : Γ  `ℕ}
     Value V
      --------------
     Value (`suc V)

此处的 zero 需要一个隐式函数来帮助类型推测,与 Lists[] 的情况类似。

规约

规约规则和之前给出的类似,除去我们必须给出每个项的类型。 如同之前,兼容性规则规约一个项的一部分,用 ξ 标出; 简化构造子与其解构子的规则用 β 标出:

infix 2 _—→_

data _—→_ :  {Γ A}  (Γ  A)  (Γ  A)  Set where

  ξ-·₁ :  {Γ A B} {L L′ : Γ  A  B} {M : Γ  A}
     L —→ L′
      ---------------
     L · M —→ L′ · M

  ξ-·₂ :  {Γ A B} {V : Γ  A  B} {M M′ : Γ  A}
     Value V
     M —→ M′
      ---------------
     V · M —→ V · M′

  β-ƛ :  {Γ A B} {N : Γ , A  B} {W : Γ  A}
     Value W
      --------------------
     (ƛ N) · W —→ N [ W ]

  ξ-suc :  {Γ} {M M′ : Γ  `ℕ}
     M —→ M′
      -----------------
     `suc M —→ `suc M′

  ξ-case :  {Γ A} {L L′ : Γ  `ℕ} {M : Γ  A} {N : Γ , `ℕ  A}
     L —→ L′
      -------------------------
     case L M N —→ case L′ M N

  β-zero :   {Γ A} {M : Γ  A} {N : Γ , `ℕ  A}
      -------------------
     case `zero M N —→ M

  β-suc :  {Γ A} {V : Γ  `ℕ} {M : Γ  A} {N : Γ , `ℕ  A}
     Value V
      ----------------------------
     case (`suc V) M N —→ N [ V ]

  β-μ :  {Γ A} {N : Γ , A  A}
      ----------------
     μ N —→ N [ μ N ]

定义中指出,M —→ N 只能由类型Γ ⊢ A 的两个项 MN 组成, 其中 Γ 是一个语境,A 是一个类型。 换句话说,我们的定义中内置了规约保存类型的证明。 我们不需要额外证明保型性。 Agda 的类型检查器检验了每个项保存了类型。 在 β 规则的情况中,保型性依赖于替换保存类型的性质,而它内置于替换的定义之中。

自反传递闭包

自反传递闭包的定义与之前完全一样。 我们直接复制粘贴之前的定义:

infix  2 _—↠_
infix  1 begin_
infixr 2 _—→⟨_⟩_
infix  3 _∎

data _—↠_ {Γ A} : (Γ  A)  (Γ  A)  Set where

  _∎ : (M : Γ  A)
      ------
     M —↠ M

  step—→ : (L : Γ  A) {M N : Γ  A}
     M —↠ N
     L —→ M
      ------
     L —↠ N

pattern _—→⟨_⟩_ L L—→M M—↠N = step—→ L M—↠N L—→M

begin_ :  {Γ A} {M N : Γ  A}
   M —↠ N
    ------
   M —↠ N
begin M—↠N = M—↠N

例子

我们重复之前的每一个例子。 首先,将 Church 法表示的二应用于后继函数和零得自然数二:

_ : twoᶜ · sucᶜ · `zero {} —↠ `suc `suc `zero
_ =
  begin
    twoᶜ · sucᶜ · `zero
  —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ (sucᶜ · (sucᶜ · # 0))) · `zero
  —→⟨ β-ƛ V-zero 
    sucᶜ · (sucᶜ · `zero)
  —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    sucᶜ · `suc `zero
  —→⟨ β-ƛ (V-suc V-zero) 
   `suc (`suc `zero)
  

和之前一样,我们需要给出为 `zero 给出显式的语境。

接下来,展示二加二得四的规约例子:

_ : plus {} · two · two —↠ `suc `suc `suc `suc `zero
_ =
    plus · two · two
  —→⟨ ξ-·₁ (ξ-·₁ β-μ) 
    (ƛ ƛ case (` S Z) (` Z) (`suc (plus · ` Z · ` S Z))) · two · two
  —→⟨ ξ-·₁ (β-ƛ (V-suc (V-suc V-zero))) 
    (ƛ case two (` Z) (`suc (plus · ` Z · ` S Z))) · two
  —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    case two two (`suc (plus · ` Z · two))
  —→⟨ β-suc (V-suc V-zero) 
    `suc (plus · `suc `zero · two)
  —→⟨ ξ-suc (ξ-·₁ (ξ-·₁ β-μ)) 
    `suc ((ƛ ƛ case (` S Z) (` Z) (`suc (plus · ` Z · ` S Z)))
      · `suc `zero · two)
  —→⟨ ξ-suc (ξ-·₁ (β-ƛ (V-suc V-zero))) 
    `suc ((ƛ case (`suc `zero) (` Z) (`suc (plus · ` Z · ` S Z))) · two)
  —→⟨ ξ-suc (β-ƛ (V-suc (V-suc V-zero))) 
    `suc (case (`suc `zero) (two) (`suc (plus · ` Z · two)))
  —→⟨ ξ-suc (β-suc V-zero) 
    `suc (`suc (plus · `zero · two))
  —→⟨ ξ-suc (ξ-suc (ξ-·₁ (ξ-·₁ β-μ))) 
    `suc (`suc ((ƛ ƛ case (` S Z) (` Z) (`suc (plus · ` Z · ` S Z)))
      · `zero · two))
  —→⟨ ξ-suc (ξ-suc (ξ-·₁ (β-ƛ V-zero))) 
    `suc (`suc ((ƛ case `zero (` Z) (`suc (plus · ` Z · ` S Z))) · two))
  —→⟨ ξ-suc (ξ-suc (β-ƛ (V-suc (V-suc V-zero)))) 
    `suc (`suc (case `zero (two) (`suc (plus · ` Z · two))))
  —→⟨ ξ-suc (ξ-suc β-zero) 
   `suc (`suc (`suc (`suc `zero)))
  

最后,用 Church 数规约同样的例子:

_ : plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero —↠ `suc `suc `suc `suc `zero {}
_ =
  begin
    plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero
  —→⟨ ξ-·₁ (ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ))) 
    (ƛ ƛ ƛ twoᶜ · ` S Z · (` S S Z · ` S Z · ` Z)) · twoᶜ · sucᶜ · `zero
  —→⟨ ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ ƛ twoᶜ · ` S Z · (twoᶜ · ` S Z · ` Z)) · sucᶜ · `zero
  —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ twoᶜ · sucᶜ · (twoᶜ · sucᶜ · ` Z)) · `zero
  —→⟨ β-ƛ V-zero 
    twoᶜ · sucᶜ · (twoᶜ · sucᶜ · `zero)
  —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ sucᶜ · (sucᶜ · ` Z)) · (twoᶜ · sucᶜ · `zero)
  —→⟨ ξ-·₂ V-ƛ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ sucᶜ · (sucᶜ · ` Z)) · ((ƛ sucᶜ · (sucᶜ · ` Z)) · `zero)
  —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    (ƛ sucᶜ · (sucᶜ · ` Z)) · (sucᶜ · (sucᶜ · `zero))
  —→⟨ ξ-·₂ V-ƛ (ξ-·₂ V-ƛ (β-ƛ V-zero)) 
    (ƛ sucᶜ · (sucᶜ · ` Z)) · (sucᶜ · `suc `zero)
  —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc V-zero)) 
    (ƛ sucᶜ · (sucᶜ · ` Z)) · `suc (`suc `zero)
  —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    sucᶜ · (sucᶜ · `suc (`suc `zero))
  —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc (V-suc V-zero))) 
    sucᶜ · `suc (`suc (`suc `zero))
  —→⟨ β-ƛ (V-suc (V-suc (V-suc V-zero))) 
    `suc (`suc (`suc (`suc `zero)))
  

值不再规约

我们现在完成了所有的定义,包含了之前证明的一些推论:替换保存类型和保型性。 我们接下来证明剩下的结论。

练习 V¬—→(习题)

使用上文的表示方法,证明值不再规约;以及它的推论:可规约的项不是值。

-- 请将代码写在此处。

可进性

和之前一样,每一个良类型的闭项要么是一个值,要么可以进行规约。 可进性的形式化与之前一样,但是附上了类型:

data Progress {A} (M :   A) : Set where

  step :  {N :   A}
     M —→ N
      ----------
     Progress M

  done :
      Value M
      ----------
     Progress M

可进性的声明和证明与之前大抵相同,加上了适当的附注:

progress :  {A}  (M :   A)  Progress M
progress (` ())
progress (ƛ N)                          =  done V-ƛ
progress (L · M) with progress L
...    | step L—→L′                     =  step (ξ-·₁ L—→L′)
...    | done V-ƛ with progress M
...        | step M—→M′                 =  step (ξ-·₂ V-ƛ M—→M′)
...        | done VM                    =  step (β-ƛ VM)
progress (`zero)                        =  done V-zero
progress (`suc M) with progress M
...    | step M—→M′                     =  step (ξ-suc M—→M′)
...    | done VM                        =  done (V-suc VM)
progress (case L M N) with progress L
...    | step L—→L′                     =  step (ξ-case L—→L′)
...    | done V-zero                    =  step (β-zero)
...    | done (V-suc VL)                =  step (β-suc VL)
progress (μ N)                          =  step (β-μ)

求值

之前,我们将保型性和可进性结合来对一个项求值。 我们在此处也可以这么做,但是我们不再需要显式地参考保型性,因为它内置于规约的定义中。

如同之前,汽油由自然数表示:

record Gas : Set where
  constructor gas
  field
    amount : 

当求值器返回项 N 时,它要么给出 N 是值的证明,要么提示汽油耗尽:

data Finished {Γ A} (N : Γ  A) : Set where

   done :
       Value N
       ----------
      Finished N

   out-of-gas :
       ----------
       Finished N

给定类型为 A 的项 L,求值器会返回一个从 L 到某个 N 的求值序列,并提示规约是否完成:

data Steps {A} :   A  Set where

  steps : {L N :   A}
     L —↠ N
     Finished N
      ----------
     Steps L

求值器取汽油和项,返回对应的步骤:

eval :  {A}
   Gas
   (L :   A)
    -----------
   Steps L
eval (gas zero)    L                     =  steps (L ) out-of-gas
eval (gas (suc m)) L with progress L
... | done VL                            =  steps (L ) (done VL)
... | step {M} L—→M with eval (gas m) M
...    | steps M—↠N fin                  =  steps (L —→⟨ L—→M  M—↠N) fin

由于我们不再需要使用保型性,定义比之前略微简单。

例子

我们重复之前的例子,重新定义无限循环的项 sucμ

sucμ :   `ℕ
sucμ = μ (`suc (# 0))

为了计算无限规约序列的前三步,我们使用满足三步的汽油:

_ : eval (gas 3) sucμ 
  steps
   (μ `suc ` Z
   —→⟨ β-μ 
    `suc (μ `suc ` Z)
   —→⟨ ξ-suc β-μ 
    `suc (`suc (μ `suc ` Z))
   —→⟨ ξ-suc (ξ-suc β-μ) 
    `suc (`suc (`suc (μ `suc ` Z)))
   )
   out-of-gas
_ = refl

将 Church 法表示的二应用于后继函数和零:

_ : eval (gas 100) (twoᶜ · sucᶜ · `zero) 
  steps
   ((ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · (ƛ `suc ` Z) · `zero
   —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ (ƛ `suc ` Z) · ((ƛ `suc ` Z) · ` Z)) · `zero
   —→⟨ β-ƛ V-zero 
    (ƛ `suc ` Z) · ((ƛ `suc ` Z) · `zero)
   —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    (ƛ `suc ` Z) · `suc `zero
   —→⟨ β-ƛ (V-suc V-zero) 
    `suc (`suc `zero)
   )
   (done (V-suc (V-suc V-zero)))
_ = refl

二加二等于四:

_ : eval (gas 100) (plus · two · two) 
  steps
   ((μ
     (ƛ
      (ƛ
       case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
    · `suc (`suc `zero)
    · `suc (`suc `zero)
   —→⟨ ξ-·₁ (ξ-·₁ β-μ) 
    (ƛ
     (ƛ
      case (` (S Z)) (` Z)
      (`suc
       ((μ
         (ƛ
          (ƛ
           case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
        · ` Z
        · ` (S Z)))))
    · `suc (`suc `zero)
    · `suc (`suc `zero)
   —→⟨ ξ-·₁ (β-ƛ (V-suc (V-suc V-zero))) 
    (ƛ
     case (`suc (`suc `zero)) (` Z)
     (`suc
      ((μ
        (ƛ
         (ƛ
          case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
       · ` Z
       · ` (S Z))))
    · `suc (`suc `zero)
   —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    case (`suc (`suc `zero)) (`suc (`suc `zero))
    (`suc
     ((μ
       (ƛ
        (ƛ
         case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
      · ` Z
      · `suc (`suc `zero)))
   —→⟨ β-suc (V-suc V-zero) 
    `suc
    ((μ
      (ƛ
       (ƛ
        case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
     · `suc `zero
     · `suc (`suc `zero))
   —→⟨ ξ-suc (ξ-·₁ (ξ-·₁ β-μ)) 
    `suc
    ((ƛ
      (ƛ
       case (` (S Z)) (` Z)
       (`suc
        ((μ
          (ƛ
           (ƛ
            case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
         · ` Z
         · ` (S Z)))))
     · `suc `zero
     · `suc (`suc `zero))
   —→⟨ ξ-suc (ξ-·₁ (β-ƛ (V-suc V-zero))) 
    `suc
    ((ƛ
      case (`suc `zero) (` Z)
      (`suc
       ((μ
         (ƛ
          (ƛ
           case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
        · ` Z
        · ` (S Z))))
     · `suc (`suc `zero))
   —→⟨ ξ-suc (β-ƛ (V-suc (V-suc V-zero))) 
    `suc
    case (`suc `zero) (`suc (`suc `zero))
    (`suc
     ((μ
       (ƛ
        (ƛ
         case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
      · ` Z
      · `suc (`suc `zero)))
   —→⟨ ξ-suc (β-suc V-zero) 
    `suc
    (`suc
     ((μ
       (ƛ
        (ƛ
         case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
      · `zero
      · `suc (`suc `zero)))
   —→⟨ ξ-suc (ξ-suc (ξ-·₁ (ξ-·₁ β-μ))) 
    `suc
    (`suc
     ((ƛ
       (ƛ
        case (` (S Z)) (` Z)
        (`suc
         ((μ
           (ƛ
            (ƛ
             case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
          · ` Z
          · ` (S Z)))))
      · `zero
      · `suc (`suc `zero)))
   —→⟨ ξ-suc (ξ-suc (ξ-·₁ (β-ƛ V-zero))) 
    `suc
    (`suc
     ((ƛ
       case `zero (` Z)
       (`suc
        ((μ
          (ƛ
           (ƛ
            case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
         · ` Z
         · ` (S Z))))
      · `suc (`suc `zero)))
   —→⟨ ξ-suc (ξ-suc (β-ƛ (V-suc (V-suc V-zero)))) 
    `suc
    (`suc
     case `zero (`suc (`suc `zero))
     (`suc
      ((μ
        (ƛ
         (ƛ
          case (` (S Z)) (` Z) (`suc (` (S (S (S Z))) · ` Z · ` (S Z))))))
       · ` Z
       · `suc (`suc `zero))))
   —→⟨ ξ-suc (ξ-suc β-zero) 
    `suc (`suc (`suc (`suc `zero)))
   )
   (done (V-suc (V-suc (V-suc (V-suc V-zero)))))
_ = refl

以及 Church 法表示的对应的项:

_ : eval (gas 100) (plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero) 
  steps
   ((ƛ
     (ƛ
      (ƛ (ƛ ` (S (S (S Z))) · ` (S Z) · (` (S (S Z)) · ` (S Z) · ` Z)))))
    · (ƛ (ƛ ` (S Z) · (` (S Z) · ` Z)))
    · (ƛ (ƛ ` (S Z) · (` (S Z) · ` Z)))
    · (ƛ `suc ` Z)
    · `zero
   —→⟨ ξ-·₁ (ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ))) 
    (ƛ
     (ƛ
      (ƛ
       (ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · ` (S Z) ·
       (` (S (S Z)) · ` (S Z) · ` Z))))
    · (ƛ (ƛ ` (S Z) · (` (S Z) · ` Z)))
    · (ƛ `suc ` Z)
    · `zero
   —→⟨ ξ-·₁ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ
     (ƛ
      (ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · ` (S Z) ·
      ((ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · ` (S Z) · ` Z)))
    · (ƛ `suc ` Z)
    · `zero
   —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ
     (ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · (ƛ `suc ` Z) ·
     ((ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · (ƛ `suc ` Z) · ` Z))
    · `zero
   —→⟨ β-ƛ V-zero 
    (ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · (ƛ `suc ` Z) ·
    ((ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · (ƛ `suc ` Z) · `zero)
   —→⟨ ξ-·₁ (β-ƛ V-ƛ) 
    (ƛ (ƛ `suc ` Z) · ((ƛ `suc ` Z) · ` Z)) ·
    ((ƛ (ƛ ` (S Z) · (` (S Z) · ` Z))) · (ƛ `suc ` Z) · `zero)
   —→⟨ ξ-·₂ V-ƛ (ξ-·₁ (β-ƛ V-ƛ)) 
    (ƛ (ƛ `suc ` Z) · ((ƛ `suc ` Z) · ` Z)) ·
    ((ƛ (ƛ `suc ` Z) · ((ƛ `suc ` Z) · ` Z)) · `zero)
   —→⟨ ξ-·₂ V-ƛ (β-ƛ V-zero) 
    (ƛ (ƛ `suc ` Z) · ((ƛ `suc ` Z) · ` Z)) ·
    ((ƛ `suc ` Z) · ((ƛ `suc ` Z) · `zero))
   —→⟨ ξ-·₂ V-ƛ (ξ-·₂ V-ƛ (β-ƛ V-zero)) 
    (ƛ (ƛ `suc ` Z) · ((ƛ `suc ` Z) · ` Z)) ·
    ((ƛ `suc ` Z) · `suc `zero)
   —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc V-zero)) 
    (ƛ (ƛ `suc ` Z) · ((ƛ `suc ` Z) · ` Z)) · `suc (`suc `zero)
   —→⟨ β-ƛ (V-suc (V-suc V-zero)) 
    (ƛ `suc ` Z) · ((ƛ `suc ` Z) · `suc (`suc `zero))
   —→⟨ ξ-·₂ V-ƛ (β-ƛ (V-suc (V-suc V-zero))) 
    (ƛ `suc ` Z) · `suc (`suc (`suc `zero))
   —→⟨ β-ƛ (V-suc (V-suc (V-suc V-zero))) 
    `suc (`suc (`suc (`suc `zero)))
   )
   (done (V-suc (V-suc (V-suc (V-suc V-zero)))))
_ = refl

我们省去规约是确定的证明,因为它很繁琐,而且与之前的证明几乎完全相同。

练习 mul-example (推荐)

使用求值器,确认二乘二等于四。

-- 请将代码写在此处。

内在类型是黄金的

代码行数是有提示性的。 这一章虽然包括了前两章涵盖的形式化内容,却用了更少的代码。 除去所有的例子,和 Properties 中出现且没有在 DeBruijn 中出现的证明 (例如规约是确定的证明),代码行数如下:

Lambda                      216
Properties                  235
DeBruijn                    276

两种方法的比例接近于黄金比例:外在类型的项代码行数大约是内在类型项的 1.6 倍。

Unicode

本章中使用了以下 Unicode:

σ  U+03C3  GREEK SMALL LETTER SIGMA (\Gs or \sigma)
₀  U+2080  SUBSCRIPT ZERO (\_0)
₃  U+20B3  SUBSCRIPT THREE (\_3)
₄  U+2084  SUBSCRIPT FOUR (\_4)
₅  U+2085  SUBSCRIPT FIVE (\_5)
₆  U+2086  SUBSCRIPT SIX (\_6)
₇  U+2087  SUBSCRIPT SEVEN (\_7)
≠  U+2260  NOT EQUAL TO (\=n)

More: 简单类型 λ-演算的更多构造

module plfa.part2.More where

到此,我们主要集中于一门基于 Plotkin 的 PCF 的相对轻便的语言,包括了函数、自然数和不动点。 在本章中,我们将扩展我们的演算,来支持下列特性:

  • 原语数字
  • let 绑定
  • 积的替代表示方法
  • 单元类型
  • 单元类型的替代表示方法
  • 空类型
  • 列表

所有上述数据类型和本书第一部分描述的相近。 对于 let 和数据类型的「替代表示方法」,我们展示如何将它们翻译至演算中其他的构造。 介绍的大部分将由非形式化的方式展示。 我们展示如何形式化前四种构造,并将其余留给读者作为习题。

非形式化的表述会以类似于 Lambda 章节中的形式,使用外在类型的项给出。 而形式化的表述会以类似于 DeBruijn 章节中的形式,使用内在类型的项给出。

到现在,使用符号来解释应该可以比文字更简洁、更精确、更易于跟随。 对于每一种构造,我们给出它的语法、赋型、规约和例子。 在需要的时候,我们也会给出构造的翻译;在下个章节我们会正式地证明翻译的正确性。

原语数字

我们定义 Nat 类型,等同于内置的自然数类型,并定义乘法作为数字的基本运算:

语法

A, B, C ::= ...                     类型
  Nat                                 原语自然数

L, M, N ::= ...                     项
  con c                               常量
  L `* M                              乘法

V, W ::= ...                        值
  con c                               常量

赋型

con 规则的前提不同寻常,它指代了一个 Agda 的赋型判断,而不是 我们定义的演算中的赋型判断。

c : ℕ
--------------- con
Γ ⊢ con c : Nat

Γ ⊢ L : Nat
Γ ⊢ M : Nat
---------------- _`*_
Γ ⊢ L `* M : Nat

规约

直接定义原语的规则,例如下面的最后一条规则,被称为 δ 规则。 这里的 δ 规则使用 Agda 标准库中的自然数乘法来定义原语数字的乘法。

L —→ L′
----------------- ξ-*₁
L `* M —→ L′ `* M

M —→ M′
----------------- ξ-*₂
V `* M —→ V `* M′

----------------------------- δ-*
con c `* con d —→ con (c * d)

例子

下面是一个求立方的函数的例子:

cube : ∅ ⊢ Nat ⇒ Nat
cube = ƛ x ⇒ x `* x `* x

Let 绑定

Let 绑定只影响项的语法;不引入新的值或者类型:

语法

L, M, N ::= ...                     项
  `let x `= M `in N                   let

赋型

Γ ⊢ M ⦂ A
Γ , x ⦂ A ⊢ N ⦂ B
------------------------- `let
Γ ⊢ `let x `= M `in N ⦂ B

规约

M —→ M′
--------------------------------------- ξ-let
`let x `= M `in N —→ `let x `= M′ `in N

--------------------------------- β-let
`let x `= V `in N —→ N [ x := V ]

下面是一个求十次方的函数:

exp10 : ∅ ⊢ Nat ⇒ Nat
exp10 = ƛ x ⇒ `let x2  `= x  `* x  `in
              `let x4  `= x2 `* x2 `in
              `let x5  `= x4 `* x  `in
              x5 `* x5

翻译

我们可以将每个 let 项翻译成一个抽象:

(`let x `= M `in N) †  =  (ƛ x ⇒ (N †)) · (M †)

此处的 M † 代表了将项 M 从带有此构造的演算到不带此构造演算的翻译。

语法

A, B, C ::= ...                     类型
  A `× B                              积类型

L, M, N ::= ...                     项
  `⟨ M , N ⟩                          有序对
  `proj₁ L                            投影第一分量
  `proj₂ L                            投影第二分量

V, W ::= ...                        值
  `⟨ V , W ⟩                          有序对

赋型

Γ ⊢ M ⦂ A
Γ ⊢ N ⦂ B
----------------------- `⟨_,_⟩ 或 `×-I
Γ ⊢ `⟨ M , N ⟩ ⦂ A `× B

Γ ⊢ L ⦂ A `× B
---------------- `proj₁ 或 `×-E₁
Γ ⊢ `proj₁ L ⦂ A

Γ ⊢ L ⦂ A `× B
---------------- `proj₂ 或 `×-E₂
Γ ⊢ `proj₂ L ⦂ B

规约

M —→ M′
------------------------- ξ-⟨,⟩₁
`⟨ M , N ⟩ —→ `⟨ M′ , N ⟩

N —→ N′
------------------------- ξ-⟨,⟩₂
`⟨ V , N ⟩ —→ `⟨ V , N′ ⟩

L —→ L′
--------------------- ξ-proj₁
`proj₁ L —→ `proj₁ L′

L —→ L′
--------------------- ξ-proj₂
`proj₂ L —→ `proj₂ L′

---------------------- β-proj₁
`proj₁ `⟨ V , W ⟩ —→ V

---------------------- β-proj₂
`proj₂ `⟨ V , W ⟩ —→ W

例子

下面是一个交换有序对中分量的函数:

swap× : ∅ ⊢ A `× B ⇒ B `× A
swap× = ƛ z ⇒ `⟨ `proj₂ z , `proj₁ z ⟩

积的替代表示方法

与其使用两种消去积类型的方法,我们可以使用一个匹配表达式来同时绑定两个变量, 作为积的替代表示方法。 我们重复完整的语法,但只给出新的赋型和规约规则:

语法

A, B, C ::= ...                     类型
  A `× B                              积累性

L, M, N ::= ...                     项
  `⟨ M , N ⟩                          有序对
  case× L [⟨ x , y ⟩⇒ M ]             匹配

V, W ::=                            值
  `⟨ V , W ⟩                          有序对

赋型

Γ ⊢ L ⦂ A `× B
Γ , x ⦂ A , y ⦂ B ⊢ N ⦂ C
------------------------------- case× 或 ×-E
Γ ⊢ case× L [⟨ x , y ⟩⇒ N ] ⦂ C

规约

L —→ L′
--------------------------------------------------- ξ-case×
case× L [⟨ x , y ⟩⇒ N ] —→ case× L′ [⟨ x , y ⟩⇒ N ]

--------------------------------------------------------- β-case×
case× `⟨ V , W ⟩ [⟨ x , y ⟩⇒ N ] —→ N [ x := V ][ y := W ]

下面是用新记法重写的交换有序对中分量的函数:

swap×-case : ∅ ⊢ A `× B ⇒ B `× A
swap×-case = ƛ z ⇒ case× z
                     [⟨ x , y ⟩⇒ `⟨ y , x ⟩ ]

翻译

我们可以将替代表示方法翻译到用投影的表示方法:

  (case× L [⟨ x , y ⟩⇒ N ]) †
=
  `let z `= (L †) `in
  `let x `= `proj₁ z `in
  `let y `= `proj₂ z `in
  (N †)

这里 z 是一个不在 N 中以自由变量出现的变量。 我们将这样的变量叫做新鲜(Fresh)的变量。

可能有人认为我们可以使用下面更紧凑的翻译:

-- 错误
  (case× L [⟨ x , y ⟩⇒ N ]) †
=
  (N †) [ x := `proj₁ (L †) ] [ y := `proj₂ (L †) ]

但是这样的话它们表现的不一样。 第一项总是在规约 N 之前规约 L,它只计算 `proj₁`proj₂一次。 第二项在规约 N 之前不先将 L 规约至值,取决于 xyN 中出现的次数, 它将规约 L 很多次或者根本不规约,因此它会计算 `proj₁`proj₂ 很多次或者根本不计算。

我们也可以反向的翻译:

(`proj₁ L) ‡  =  case× (L ‡) [⟨ x , y ⟩⇒ x ]
(`proj₂ L) ‡  =  case× (L ‡) [⟨ x , y ⟩⇒ y ]

语法

A, B, C ::= ...                     类型
  A `⊎ B                              和类型

L, M, N ::= ...                     项
  `inj₁ M                             注入第一分量
  `inj₂ N                             注入第二分量
  case⊎ L [inj₁ x ⇒ M |inj₂ y ⇒ N ]   匹配

V, W ::= ...                        值
  `inj₁ V                             注入第一分量
  `inj₂ W                             注入第二分量

赋型

Γ ⊢ M ⦂ A
-------------------- `inj₁ 或 ⊎-I₁
Γ ⊢ `inj₁ M ⦂ A `⊎ B

Γ ⊢ N ⦂ B
-------------------- `inj₂ 或 ⊎-I₂
Γ ⊢ `inj₂ N ⦂ A `⊎ B

Γ ⊢ L ⦂ A `⊎ B
Γ , x ⦂ A ⊢ M ⦂ C
Γ , y ⦂ B ⊢ N ⦂ C
----------------------------------------- case⊎ 或 ⊎-E
Γ ⊢ case⊎ L [inj₁ x ⇒ M |inj₂ y ⇒ N ] ⦂ C

规约

M —→ M′
------------------- ξ-inj₁
`inj₁ M —→ `inj₁ M′

N —→ N′
------------------- ξ-inj₂
`inj₂ N —→ `inj₂ N′

L —→ L′
---------------------------------------------------------------------- ξ-case⊎
case⊎ L [inj₁ x ⇒ M |inj₂ y ⇒ N ] —→ case⊎ L′ [inj₁ x ⇒ M |inj₂ y ⇒ N ]

--------------------------------------------------------- β-inj₁
case⊎ (`inj₁ V) [inj₁ x ⇒ M |inj₂ y ⇒ N ] —→ M [ x := V ]

--------------------------------------------------------- β-inj₂
case⊎ (`inj₂ W) [inj₁ x ⇒ M |inj₂ y ⇒ N ] —→ N [ y := W ]

例子

下面是交换和的两个分量的函数:

swap⊎ : ∅ ⊢ A `⊎ B ⇒ B `⊎ A
swap⊎ = ƛ z ⇒ case⊎ z
                [inj₁ x ⇒ `inj₂ x
                |inj₂ y ⇒ `inj₁ y ]

单元类型

对于单元类型来说,有一种方法引入单元类型,但是没有消去单元类型的方法。 单元类型没有规约规则。

语法

A, B, C ::= ...                     类型
  `⊤                                  单元类型

L, M, N ::= ...                     项
  `tt                                 单元值

V, W ::= ...                        值
  `tt                                 单元值

赋型

------------ `tt 或 ⊤-I
Γ ⊢ `tt ⦂ `⊤

规约

(无)

例子

下面是 AA `× `⊤的同构:

to×⊤ : ∅ ⊢ A ⇒ A `× `⊤
to×⊤ = ƛ x ⇒ `⟨ x , `tt ⟩

from×⊤ : ∅ ⊢ A `× `⊤ ⇒ A
from×⊤ = ƛ z ⇒ `proj₁ z

单元类型的替代表达方法

与其没有消去单元类型的方法,我们可以使用一个匹配表达式来绑定零个变量, 作为单元类型的替代表示方法。 我们重复完整的语法,但只给出新的赋型和规约规则:

语法

A, B, C ::= ...                     类型
  `⊤                                  单元类型

L, M, N ::= ...                     项
  `tt                                 单元值
  `case⊤ L [tt⇒ N ]                   匹配

V, W ::= ...                        值
  `tt                                 单元值

赋型

Γ ⊢ L ⦂ `⊤
Γ ⊢ M ⦂ A
------------------------ case⊤ 或者 ⊤-E
Γ ⊢ case⊤ L [tt⇒ M ] ⦂ A

规约

L —→ L′
------------------------------------- ξ-case⊤
case⊤ L [tt⇒ M ] —→ case⊤ L′ [tt⇒ M ]

----------------------- β-case⊤
case⊤ `tt [tt⇒ M ] —→ M

下面是用新记法重新 AA ‵× `⊤ 的同构的一半:

from×⊤-case : ∅ ⊢ A `× `⊤ ⇒ A
from×⊤-case = ƛ z ⇒ case× z
                      [⟨ x , y ⟩⇒ case⊤ y
                                    [tt⇒ x ] ]

翻译

我们可以将替代表示方法翻译到用没有匹配式的表示方法:

(case⊤ L [tt⇒ M ]) †  =  `let z `= (L †) `in (M †)

此处 z 是一个在 M 中不以自由变量出现的变量。

空类型

对于空类型来说,只有一种消去此类型的值的方法,但是没有引入此类型的值的方法。 没有空类型的值,也没有规约规则,但是有 β 规则。 case⊥ 构造和 Agda 中的 ⊥-elim 的作用相似:

语法

A, B, C ::= ...                     类型
  `⊥                                  空类型

L, M, N ::= ...                     项
  case⊥ L []                          匹配

赋型

Γ ⊢ L ⦂ `⊥
------------------ case⊥ 或 ⊥-E
Γ ⊢ case⊥ L [] ⦂ A

规约

L —→ L′
------------------------- ξ-case⊥
case⊥ L [] —→ case⊥ L′ []

例子

下面是 AA `⊎ `⊥ 的同构:

to⊎⊥ : ∅ ⊢ A ⇒ A `⊎ `⊥
to⊎⊥ = ƛ x ⇒ `inj₁ x

from⊎⊥ : ∅ ⊢ A `⊎ `⊥ ⇒ A
from⊎⊥ = ƛ z ⇒ case⊎ z
                 [inj₁ x ⇒ x
                 |inj₂ y ⇒ case⊥ y
                             [] ]

列表

语法

A, B, C ::= ...                     类型
  `List A                             列表类型

L, M, N ::= ...                     项
  `[]                                 空列表
  M `∷ N                              构造列表
  caseL L [[]⇒ M | x ∷ y ⇒ N ]        匹配

V, W ::= ...                        值
  `[]                                 空列表
  V `∷ W                              构造列表

赋型

----------------- `[] 或 List-I₁
Γ ⊢ `[] ⦂ `List A

Γ ⊢ M ⦂ A
Γ ⊢ N ⦂ `List A
-------------------- _`∷_ 或 List-I₂
Γ ⊢ M `∷ N ⦂ `List A

Γ ⊢ L ⦂ `List A
Γ ⊢ M ⦂ B
Γ , x ⦂ A , xs ⦂ `List A ⊢ N ⦂ B
-------------------------------------- caseL 或 List-E
Γ ⊢ caseL L [[]⇒ M | x ∷ xs ⇒ N ] ⦂ B

规约

M —→ M′
----------------- ξ-∷₁
M `∷ N —→ M′ `∷ N

N —→ N′
----------------- ξ-∷₂
V `∷ N —→ V `∷ N′

L —→ L′
--------------------------------------------------------------- ξ-caseL
caseL L [[]⇒ M | x ∷ xs ⇒ N ] —→ caseL L′ [[]⇒ M | x ∷ xs ⇒ N ]

------------------------------------ β-[]
caseL `[] [[]⇒ M | x ∷ xs ⇒ N ] —→ M

--------------------------------------------------------------- β-∷
caseL (V `∷ W) [[]⇒ M | x ∷ xs ⇒ N ] —→ N [ x := V ][ xs := W ]

例子

下面是列表的映射函数:

mapL : ∅ ⊢ (A ⇒ B) ⇒ `List A ⇒ `List B
mapL = μ mL ⇒ ƛ f ⇒ ƛ xs ⇒
         caseL xs
           [[]⇒ `[]
           | x ∷ xs ⇒ f · x `∷ mL · f · xs ]

形式化

我们接下来展示如何形式化:

  • 原语数字
  • let 绑定
  • 积的替代表示方法

其余构造的形式化作为练习留给读者。

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl)
open import Data.Empty using (; ⊥-elim)
open import Data.Nat using (; zero; suc; _*_; _<_; _≤?_; z≤n; s≤s)
open import Relation.Nullary using (¬_)
open import Relation.Nullary.Decidable using (True; toWitness)

语法

infix  4 _⊢_
infix  4 _∋_
infixl 5 _,_

infixr 7 _⇒_
infixr 9 _`×_

infix  5 ƛ_
infix  5 μ_
infixl 7 _·_
infixl 8 _`*_
infix  8 `suc_
infix  9 `_
infix  9 S_
infix  9 #_

类型

data Type : Set where
  `ℕ    : Type
  _⇒_   : Type  Type  Type
  Nat   : Type
  _`×_  : Type  Type  Type

语境

data Context : Set where
     : Context
  _,_ : Context  Type  Context

变量及查询判断

data _∋_ : Context  Type  Set where

  Z :  {Γ A}
      ---------
     Γ , A  A

  S_ :  {Γ A B}
     Γ  B
      ---------
     Γ , A  B

项以及赋型判断

data _⊢_ : Context  Type  Set where

  -- 变量

  `_ :  {Γ A}
     Γ  A
      -----
     Γ  A

  -- 函数

  ƛ_  :   {Γ A B}
     Γ , A  B
      ---------
     Γ  A  B

  _·_ :  {Γ A B}
     Γ  A  B
     Γ  A
      ---------
     Γ  B

  -- 自然数

  `zero :  {Γ}
      ------
     Γ  `ℕ

  `suc_ :  {Γ}
     Γ  `ℕ
      ------
     Γ  `ℕ

  case :  {Γ A}
     Γ  `ℕ
     Γ  A
     Γ , `ℕ  A
      -----
     Γ  A

  -- 不动点

  μ_ :  {Γ A}
     Γ , A  A
      ----------
     Γ  A

  -- 原语数字

  con :  {Γ}
     
      -------
     Γ  Nat

  _`*_ :  {Γ}
     Γ  Nat
     Γ  Nat
      -------
     Γ  Nat

  -- let

  `let :  {Γ A B}
     Γ  A
     Γ , A  B
      ----------
     Γ  B

  -- 积

  `⟨_,_⟩ :  {Γ A B}
     Γ  A
     Γ  B
      -----------
     Γ  A  B

  `proj₁ :  {Γ A B}
     Γ  A  B
      -----------
     Γ  A

  `proj₂ :  {Γ A B}
     Γ  A  B
      -----------
     Γ  B

  -- 积的替代表示方法

  case× :  {Γ A B C}
     Γ  A  B
     Γ , A , B  C
      --------------
     Γ  C

缩减 de Bruijn 因子

length : Context  
length         =  zero
length (Γ , _)  =  suc (length Γ)

lookup : {Γ : Context}  {n : }  (p : n < length Γ)  Type
lookup {(_ , A)} {zero}    (s≤s z≤n)  =  A
lookup {(Γ , _)} {(suc n)} (s≤s p)    =  lookup p

count :  {Γ}  {n : }  (p : n < length Γ)  Γ  lookup p
count {_ , _} {zero}    (s≤s z≤n)  =  Z
count {Γ , _} {(suc n)} (s≤s p)    =  S (count p)

#_ :  {Γ}
   (n : )
   {n∈Γ : True (suc n ≤? length Γ)}
    --------------------------------
   Γ  lookup (toWitness n∈Γ)
#_ n {n∈Γ}  =  ` count (toWitness n∈Γ)

重命名

ext :  {Γ Δ}
   (∀ {A}        Γ  A      Δ  A)
    ---------------------------------
   (∀ {A B}  Γ , A  B  Δ , A  B)
ext ρ Z      =  Z
ext ρ (S x)  =  S (ρ x)

rename :  {Γ Δ}
   (∀ {A}  Γ  A  Δ  A)
    -----------------------
   (∀ {A}  Γ  A  Δ  A)
rename ρ (` x)          =  ` (ρ x)
rename ρ (ƛ N)          =  ƛ (rename (ext ρ) N)
rename ρ (L · M)        =  (rename ρ L) · (rename ρ M)
rename ρ (`zero)        =  `zero
rename ρ (`suc M)       =  `suc (rename ρ M)
rename ρ (case L M N)   =  case (rename ρ L) (rename ρ M) (rename (ext ρ) N)
rename ρ (μ N)          =  μ (rename (ext ρ) N)
rename ρ (con n)        =  con n
rename ρ (M `* N)       =  rename ρ M `* rename ρ N
rename ρ (`let M N)     =  `let (rename ρ M) (rename (ext ρ) N)
rename ρ `⟨ M , N      =  `⟨ rename ρ M , rename ρ N 
rename ρ (`proj₁ L)     =  `proj₁ (rename ρ L)
rename ρ (`proj₂ L)     =  `proj₂ (rename ρ L)
rename ρ (case× L M)    =  case× (rename ρ L) (rename (ext (ext ρ)) M)

同时代换

exts :  {Γ Δ}  (∀ {A}  Γ  A  Δ  A)  (∀ {A B}  Γ , A  B  Δ , A  B)
exts σ Z      =  ` Z
exts σ (S x)  =  rename S_ (σ x)

subst :  {Γ Δ}  (∀ {C}  Γ  C  Δ  C)  (∀ {C}  Γ  C  Δ  C)
subst σ (` k)          =  σ k
subst σ (ƛ N)          =  ƛ (subst (exts σ) N)
subst σ (L · M)        =  (subst σ L) · (subst σ M)
subst σ (`zero)        =  `zero
subst σ (`suc M)       =  `suc (subst σ M)
subst σ (case L M N)   =  case (subst σ L) (subst σ M) (subst (exts σ) N)
subst σ (μ N)          =  μ (subst (exts σ) N)
subst σ (con n)        =  con n
subst σ (M `* N)       =  subst σ M `* subst σ N
subst σ (`let M N)     =  `let (subst σ M) (subst (exts σ) N)
subst σ `⟨ M , N      =  `⟨ subst σ M , subst σ N 
subst σ (`proj₁ L)     =  `proj₁ (subst σ L)
subst σ (`proj₂ L)     =  `proj₂ (subst σ L)
subst σ (case× L M)    =  case× (subst σ L) (subst (exts (exts σ)) M)

单个和双重代换

_[_] :  {Γ A B}
   Γ , B  A
   Γ  B
    ---------
   Γ  A
_[_] {Γ} {A} {B} N M =  subst {Γ , B} {Γ} σ {A} N
  where
  σ :  {A}  Γ , B  A  Γ  A
  σ Z      =  M
  σ (S x)  =  ` x

_[_][_] :  {Γ A B C}
   Γ , A , B  C
   Γ  A
   Γ  B
    -------------
   Γ  C
_[_][_] {Γ} {A} {B} N V W =  subst {Γ , A , B} {Γ} σ N
  where
  σ :  {C}  Γ , A , B  C  Γ  C
  σ Z          =  W
  σ (S Z)      =  V
  σ (S (S x))  =  ` x

data Value :  {Γ A}  Γ  A  Set where

  -- 函数

  V-ƛ :  {Γ A B} {N : Γ , A  B}
      ---------------------------
     Value (ƛ N)

  -- 自然数

  V-zero :  {Γ}
      -----------------
     Value (`zero {Γ})

  V-suc_ :  {Γ} {V : Γ  `ℕ}
     Value V
      --------------
     Value (`suc V)

  -- 原语数字

  V-con :  {Γ n}
      -----------------
     Value (con {Γ} n)

  -- 积

  V-⟨_,_⟩ :  {Γ A B} {V : Γ  A} {W : Γ  B}
     Value V
     Value W
      ----------------
     Value `⟨ V , W 

在给出的参数无法确定隐式参数时,我们需要给出隐式参数。

规约

infix 2 _—→_

data _—→_ :  {Γ A}  (Γ  A)  (Γ  A)  Set where

  -- 函数

  ξ-·₁ :  {Γ A B} {L L′ : Γ  A  B} {M : Γ  A}
     L —→ L′
      ---------------
     L · M —→ L′ · M

  ξ-·₂ :  {Γ A B} {V : Γ  A  B} {M M′ : Γ  A}
     Value V
     M —→ M′
      ---------------
     V · M —→ V · M′

  β-ƛ :  {Γ A B} {N : Γ , A  B} {V : Γ  A}
     Value V
      --------------------
     (ƛ N) · V —→ N [ V ]

  -- 自然数

  ξ-suc :  {Γ} {M M′ : Γ  `ℕ}
     M —→ M′
      -----------------
     `suc M —→ `suc M′

  ξ-case :  {Γ A} {L L′ : Γ  `ℕ} {M : Γ  A} {N : Γ , `ℕ  A}
     L —→ L′
      -------------------------
     case L M N —→ case L′ M N

  β-zero :   {Γ A} {M : Γ  A} {N : Γ , `ℕ  A}
      -------------------
     case `zero M N —→ M

  β-suc :  {Γ A} {V : Γ  `ℕ} {M : Γ  A} {N : Γ , `ℕ  A}
     Value V
      ----------------------------
     case (`suc V) M N —→ N [ V ]

  -- 不动点

  β-μ :  {Γ A} {N : Γ , A  A}
      ----------------
     μ N —→ N [ μ N ]

  -- 原语数字

  ξ-*₁ :  {Γ} {L L′ M : Γ  Nat}
     L —→ L′
      -----------------
     L `* M —→ L′ `* M

  ξ-*₂ :  {Γ} {V M M′ : Γ  Nat}
     Value V
     M —→ M′
      -----------------
     V `* M —→ V `* M′

  δ-* :  {Γ c d}
      ---------------------------------
     con {Γ} c `* con d —→ con (c * d)

  -- let

  ξ-let :  {Γ A B} {M M′ : Γ  A} {N : Γ , A  B}
     M —→ M′
      ---------------------
     `let M N —→ `let M′ N

  β-let :  {Γ A B} {V : Γ  A} {N : Γ , A  B}
     Value V
      -------------------
     `let V N —→ N [ V ]

  -- 积

  ξ-⟨,⟩₁ :  {Γ A B} {M M′ : Γ  A} {N : Γ  B}
     M —→ M′
      -------------------------
     `⟨ M , N  —→ `⟨ M′ , N 

  ξ-⟨,⟩₂ :  {Γ A B} {V : Γ  A} {N N′ : Γ  B}
     Value V
     N —→ N′
      -------------------------
     `⟨ V , N  —→ `⟨ V , N′ 

  ξ-proj₁ :  {Γ A B} {L L′ : Γ  A  B}
     L —→ L′
      ---------------------
     `proj₁ L —→ `proj₁ L′

  ξ-proj₂ :  {Γ A B} {L L′ : Γ  A  B}
     L —→ L′
      ---------------------
     `proj₂ L —→ `proj₂ L′

  β-proj₁ :  {Γ A B} {V : Γ  A} {W : Γ  B}
     Value V
     Value W
      ----------------------
     `proj₁ `⟨ V , W  —→ V

  β-proj₂ :  {Γ A B} {V : Γ  A} {W : Γ  B}
     Value V
     Value W
      ----------------------
     `proj₂ `⟨ V , W  —→ W

  -- 积的替代表示方式

  ξ-case× :  {Γ A B C} {L L′ : Γ  A  B} {M : Γ , A , B  C}
     L —→ L′
      -----------------------
     case× L M —→ case× L′ M

  β-case× :  {Γ A B C} {V : Γ  A} {W : Γ  B} {M : Γ , A , B  C}
     Value V
     Value W
      ----------------------------------
     case× `⟨ V , W  M —→ M [ V ][ W ]

自反传递闭包

infix  2 _—↠_
infix  1 begin_
infixr 2 _—→⟨_⟩_
infix  3 _∎

data _—↠_ {Γ A} : (Γ  A)  (Γ  A)  Set where

  _∎ : (M : Γ  A)
      ------
     M —↠ M

  step—→ : (L : Γ  A) {M N : Γ  A}
     M —↠ N
     L —→ M
      ------
     L —↠ N

pattern _—→⟨_⟩_ L L—→M M—↠N = step—→ L M—↠N L—→M

begin_ :  {Γ A} {M N : Γ  A}
   M —↠ N
    ------
   M —↠ N
begin M—↠N = M—↠N

值不再规约

V¬—→ :  {Γ A} {M N : Γ  A}
   Value M
    ----------
   ¬ (M —→ N)
V¬—→ V-ƛ          ()
V¬—→ V-zero       ()
V¬—→ (V-suc VM)   (ξ-suc M—→M′)     =  V¬—→ VM M—→M′
V¬—→ V-con        ()
V¬—→ V-⟨ VM , _  (ξ-⟨,⟩₁ M—→M′)    =  V¬—→ VM M—→M′
V¬—→ V-⟨ _ , VN  (ξ-⟨,⟩₂ _ N—→N′)  =  V¬—→ VN N—→N′

可进性

data Progress {A} (M :   A) : Set where

  step :  {N :   A}
     M —→ N
      ----------
     Progress M

  done :
      Value M
      ----------
     Progress M

progress :  {A}
   (M :   A)
    -----------
   Progress M
progress (` ())
progress (ƛ N)                              =  done V-ƛ
progress (L · M) with progress L
...    | step L—→L′                         =  step (ξ-·₁ L—→L′)
...    | done V-ƛ with progress M
...        | step M—→M′                     =  step (ξ-·₂ V-ƛ M—→M′)
...        | done VM                        =  step (β-ƛ VM)
progress (`zero)                            =  done V-zero
progress (`suc M) with progress M
...    | step M—→M′                         =  step (ξ-suc M—→M′)
...    | done VM                            =  done (V-suc VM)
progress (case L M N) with progress L
...    | step L—→L′                         =  step (ξ-case L—→L′)
...    | done V-zero                        =  step β-zero
...    | done (V-suc VL)                    =  step (β-suc VL)
progress (μ N)                              =  step β-μ
progress (con n)                            =  done V-con
progress (L `* M) with progress L
...    | step L—→L′                         =  step (ξ-*₁ L—→L′)
...    | done V-con with progress M
...        | step M—→M′                     =  step (ξ-*₂ V-con M—→M′)
...        | done V-con                     =  step δ-*
progress (`let M N) with progress M
...    | step M—→M′                         =  step (ξ-let M—→M′)
...    | done VM                            =  step (β-let VM)
progress `⟨ M , N  with progress M
...    | step M—→M′                         =  step (ξ-⟨,⟩₁ M—→M′)
...    | done VM with progress N
...        | step N—→N′                     =  step (ξ-⟨,⟩₂ VM N—→N′)
...        | done VN                        =  done (V-⟨ VM , VN )
progress (`proj₁ L) with progress L
...    | step L—→L′                         =  step (ξ-proj₁ L—→L′)
...    | done (V-⟨ VM , VN )               =  step (β-proj₁ VM VN)
progress (`proj₂ L) with progress L
...    | step L—→L′                         =  step (ξ-proj₂ L—→L′)
...    | done (V-⟨ VM , VN )               =  step (β-proj₂ VM VN)
progress (case× L M) with progress L
...    | step L—→L′                         =  step (ξ-case× L—→L′)
...    | done (V-⟨ VM , VN )               =  step (β-case× VM VN)

求值

record Gas : Set where
  constructor gas
  field
    amount : 

data Finished {Γ A} (N : Γ  A) : Set where

   done :
       Value N
       ----------
      Finished N

   out-of-gas :
       ----------
       Finished N

data Steps {A} :   A  Set where

  steps : {L N :   A}
     L —↠ N
     Finished N
      ----------
     Steps L

eval :  {A}
   Gas
   (L :   A)
    -----------
   Steps L
eval (gas zero)    L                     =  steps (L ) out-of-gas
eval (gas (suc m)) L with progress L
... | done VL                            =  steps (L ) (done VL)
... | step {M} L—→M with eval (gas m) M
...    | steps M—↠N fin                  =  steps (L —→⟨ L—→M  M—↠N) fin

例子

cube :   Nat  Nat
cube = ƛ (# 0 `* # 0 `* # 0)

_ : cube · con 2 —↠ con 8
_ =
  begin
    cube · con 2
  —→⟨ β-ƛ V-con 
    con 2 `* con 2 `* con 2
  —→⟨ ξ-*₁ δ-* 
    con 4 `* con 2
  —→⟨ δ-* 
    con 8
  

exp10 :   Nat  Nat
exp10 = ƛ (`let (# 0 `* # 0)
            (`let (# 0 `* # 0)
              (`let (# 0 `* # 2)
                (# 0 `* # 0))))

_ : exp10 · con 2 —↠ con 1024
_ =
  begin
    exp10 · con 2
  —→⟨ β-ƛ V-con 
    `let (con 2 `* con 2) (`let (# 0 `* # 0) (`let (# 0 `* con 2) (# 0 `* # 0)))
  —→⟨ ξ-let δ-* 
    `let (con 4) (`let (# 0 `* # 0) (`let (# 0 `* con 2) (# 0 `* # 0)))
  —→⟨ β-let V-con 
    `let (con 4 `* con 4) (`let (# 0 `* con 2) (# 0 `* # 0))
  —→⟨ ξ-let δ-* 
    `let (con 16) (`let (# 0 `* con 2) (# 0 `* # 0))
  —→⟨ β-let V-con 
    `let (con 16 `* con 2) (# 0 `* # 0)
  —→⟨ ξ-let δ-* 
    `let (con 32) (# 0 `* # 0)
  —→⟨ β-let V-con 
    con 32 `* con 32
  —→⟨ δ-* 
    con 1024
  

swap× :  {A B}    A  B  B  A
swap× = ƛ `⟨ `proj₂ (# 0) , `proj₁ (# 0) 

_ : swap× · `⟨ con 42 , `zero  —↠ `⟨ `zero , con 42 
_ =
  begin
    swap× · `⟨ con 42 , `zero 
  —→⟨ β-ƛ V-⟨ V-con , V-zero  
    `⟨ `proj₂ `⟨ con 42 , `zero  , `proj₁ `⟨ con 42 , `zero  
  —→⟨ ξ-⟨,⟩₁ (β-proj₂ V-con V-zero) 
    `⟨ `zero , `proj₁ `⟨ con 42 , `zero  
  —→⟨ ξ-⟨,⟩₂ V-zero (β-proj₁ V-con V-zero) 
    `⟨ `zero , con 42 
  

swap×-case :  {A B}    A  B  B  A
swap×-case = ƛ case× (# 0) `⟨ # 0 , # 1 

_ : swap×-case · `⟨ con 42 , `zero  —↠ `⟨ `zero , con 42 
_ =
  begin
     swap×-case · `⟨ con 42 , `zero 
   —→⟨ β-ƛ V-⟨ V-con , V-zero  
     case× `⟨ con 42 , `zero  `⟨ # 0 , # 1 
   —→⟨ β-case× V-con V-zero 
     `⟨ `zero , con 42 
   
练习 More (推荐和实践)

形式化本章中定义的剩余构造。 修改本文件来完成你的改动。 求值每一个例子,如果需要时将其应用于数据,来确认它返回期待的答案:

  • 和(推荐)
  • 单元类型(实践)
  • 单元类型的替代表示方法(实践)
  • 空类型(推荐)
  • 列表(实践)

用下面的分隔符来标出你添加的代码:

-- begin
-- end
练习 double-subst(延伸)

证明双重代换等同于两个单独代换。

postulate
  double-subst :
     {Γ A B C} {V : Γ  A} {W : Γ  B} {N : Γ , A , B  C} 
      N [ V ][ W ]  (N [ rename S_ W ]) [ V ]

注意到我们需要交换参数,而且 W 的语境需要用重命名来调整,使得右手边的项保持良类型。

测试例子

我们重复 DeBruijn 章节中的测试例子, 来保证我们在扩展基本演算时没有破坏原演算的任何性质。

two :  {Γ}  Γ  `ℕ
two = `suc `suc `zero

plus :  {Γ}  Γ  `ℕ  `ℕ  `ℕ
plus = μ ƛ ƛ (case (# 1) (# 0) (`suc (# 3 · # 0 · # 1)))

2+2 :  {Γ}  Γ  `ℕ
2+2 = plus · two · two

Ch : Type  Type
Ch A  =  (A  A)  A  A

twoᶜ :  {Γ A}  Γ  Ch A
twoᶜ = ƛ ƛ (# 1 · (# 1 · # 0))

plusᶜ :  {Γ A}  Γ  Ch A  Ch A  Ch A
plusᶜ = ƛ ƛ ƛ ƛ (# 3 · # 1 · (# 2 · # 1 · # 0))

sucᶜ :  {Γ}  Γ  `ℕ  `ℕ
sucᶜ = ƛ `suc (# 0)

2+2ᶜ :  {Γ}  Γ  `ℕ
2+2ᶜ = plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero

Unicode

本章中使用了以下 Unicode:

σ  U+03C3  GREEK SMALL LETTER SIGMA (\Gs or \sigma)
†  U+2020  DAGGER (\dag)
‡  U+2021  DOUBLE DAGGER (\ddag)

Bisimulation: 联系不同的规约系统

module plfa.part2.Bisimulation where

有一些构造可以用其他的构造来定义。 在上一章中,我们展示了 let 项可以被重写为抽象的应用,以及积的两种不同表示方法 —— 一种使用投影,一种使用匹配表达式。 本章中,我们来看看如何形式化这些断言。

给定两个不同的系统,它们有不同的项和不同的规约规则,我们定义诸如 『一个系统模拟(Simulate)了另一个系统』此类断言的意义。 假设两个系统分别叫做(Source)和目标(Target),我们用 MN 表示源系统的项,M†N† 表示目标系统的项。 我们定义一个关系

M ~ M†

于两个系统对应的项之上。 如果每个源系统的每一个规约都有一个对应的规约序列,那我们就有了一个模拟

模拟:对于任意的 MM†N, 如果 M ~ M†M —→ N 成立,那么 M† —↠ N†N ~ N† 对于一些 N† 成立。

或者,用图表来表示:

M  --- —→ --- N
|             |
|             |
~             ~
|             |
|             |
M† --- —↠ --- N†

有时我们会使用一个更强的条件,使得每个源系统的规约对应目标系统的规约(而不是规约序列):

M  --- —→ --- N
|             |
|             |
~             ~
|             |
|             |
M† --- —→ --- N†

这个更强的条件被称为锁步(Lock-step)或者准确无误(On the nose)的模拟。

『译注:On the nose 本义为在鼻子之上,用于表述准确无误。』

如果从目标系统到源系统也有一个模拟:即每个目标系统的规约在源目标系统中有对应的规约序列, 我们对这样的情况尤其感兴趣。换句话说,~ 是一个从源到目标的模拟,而 ~ 的逆是一个从目标的源的模拟。这样的情况被称为互模拟(Bisimulation)。 (In general, if < and > are arbitrary relations such that x < y if and only if y > x then we say that < and > are converse relations. Hence, if ~ is a relation from source to target, its converse is a relation from target to source.)

要建立模拟,我们需要分情况讨论所有的可能规约,以及所有它们可能联系的项。 对于每个源系统中规约的步骤,我们必须给出目标系统中对应的规约系列。

譬如说,源系统可以是带 let 项的 λ 演算,而目标系统中的 let 被翻译了。 定义这个关系的关键规则是:

M ~ M†
N ~ N†
--------------------------------
let x = M in N ~ (ƛ x ⇒ N†) · M†

其他的规则都是合同性的规则:变量于它们自身相关,抽象与应用在它们的组成部分分别相关时相关:

-----
x ~ x

N ~ N†
------------------
ƛ x ⇒ N ~ ƛ x ⇒ N†

L ~ L†
M ~ M†
---------------
L · M ~ L† · M†

讨论语言中的其他构造——自然数、不动点和积等——对我们本章的重点意义不大, 我们节约长度而不讨论。

在此情况下,关系的定义可以用一个从源至目标的函数来制定:

(x) †               =  x
(ƛ x ⇒ N) †         =  ƛ x ⇒ (N †)
(L · M) †           =  (L †) · (M †)
(let x = M in N) †  =  (ƛ x ⇒ (N †)) · (M †)

我们有:

M † ≡ N
-------
M ~ N

以及其逆命题。 但一般情况下,我们可能有一个关系而不一定有对应的函数。

本章形式化上文中定义的 ~ 关系是一个从源系统至目标系统的模拟。 我们将反向的证明留作习题。 另一个习题是证明 More 章节中积的替代表示方法形成了一个互模拟。

导入

我们从 More 章节中导入源语言:

open import plfa.part2.More

模拟

我们直接地形式化导言中的模拟:

infix  4 _~_
infix  5 ~ƛ_
infix  7 _~·_

data _~_ :  {Γ A}  (Γ  A)  (Γ  A)  Set where

  ~` :  {Γ A} {x : Γ  A}
     ---------
    ` x ~ ` x

  ~ƛ_ :  {Γ A B} {N N† : Γ , A  B}
     N ~ N†
      ----------
     ƛ N ~ ƛ N†

  _~·_ :  {Γ A B} {L L† : Γ  A  B} {M M† : Γ  A}
     L ~ L†
     M ~ M†
      ---------------
     L · M ~ L† · M†

  ~let :  {Γ A B} {M M† : Γ  A} {N N† : Γ , A  B}
     M ~ M†
     N ~ N†
      ----------------------
     `let M N ~ (ƛ N†) · M†

More 章节中的语言有更多的构造,我们可以简单地扩充上述定义。 但是,使上述的定义小而精简让我们注重本章的重点。 虽然我们有一个较大的源语言,但我们只在模拟中考虑我们感兴趣的项,这是一个方便的小窍门。

练习 _† (实践)

形式化导言中给定的源语言至目标语言的翻译。 证明 M † ≡ N 蕴含了 M ~ N,以及其逆命题。

提示:为了简洁,我们只注重语言中的一小部分构造,所以 _† 的定义只需要注重相关的部分。 达成此目的的一种方法是用互映证明定义一个谓词来选出 _† 定义域中的项。

-- 在这里写出你的代码。

模拟与值可交换

我们需要一系列技术结果。首先是模拟与值可交换(Commute)。 即:若 M ~ M†M 是一个值,那么 M† 也是一个值。

~val :  {Γ A} {M M† : Γ  A}
   M ~ M†
   Value M
    --------
   Value M†
~val ~`           ()
~val ( ~N)      V-ƛ  =  V-ƛ
~val (~L  ~M)   ()
~val (~let ~M ~N) ()

这是一个直接的分情况讨论,唯一有意思的情况是 λ 抽象。

Exercise ~val⁻¹ (实践)

证明此命题在反方向亦成立: 若 M ~ M†Value M†,那么 Value M 成立。

-- 在这里写出你的代码。

模拟与重命名可交换

下一个技术结果是模拟和重命名可交换。 即:若 ρ 将任意的判断 Γ ∋ A 映射至另一个判断 Δ ∋ A,且 M ~ M†,那么 rename ρ M ~ rename ρ M†

~rename :  {Γ Δ}
   (ρ :  {A}  Γ  A  Δ  A)
    ----------------------------------------------------------
   (∀ {A} {M M† : Γ  A}  M ~ M†  rename ρ M ~ rename ρ M†)
~rename ρ (~`)          =  ~`
~rename ρ ( ~N)       =   (~rename (ext ρ) ~N)
~rename ρ (~L  ~M)    =  (~rename ρ ~L)  (~rename ρ ~M)
~rename ρ (~let ~M ~N)  =  ~let (~rename ρ ~M) (~rename (ext ρ) ~N)

此证明的结构与重命名的结构相似: 使用递归来重新构造每一个项,并在需要时扩充语境(在这里,我们只需要在 λ 抽象的抽象体中使用)。

模拟与替换可交换

第三个技术结果是模拟与替换可交换。 这个结果比重命名更复杂,因为我们之前只有一个重命名映射 ρ, 而现在我们需要两个替换映射 σσ†

这个证明首先需要我们构造一个类似于扩充的概念。 如果 σσ† 都将任何判断 Γ ∋ A 映射至一个判断 Δ ⊢ A, 且对于 Γ ∋ A 中的任意 x,我们有 σ x ~ σ† x, 那么对于任何 Γ , B ∋ A 中的 x,我们有 exts σ x ~ exts σ† x

~exts :  {Γ Δ}
   {σ  :  {A}  Γ  A  Δ  A}
   {σ† :  {A}  Γ  A  Δ  A}
   (∀ {A}  (x : Γ  A)  σ x ~ σ† x)
    --------------------------------------------------
   (∀ {A B}  (x : Γ , B  A)  exts σ x ~ exts σ† x)
~exts  Z      =  ~`
~exts  (S x)  =  ~rename S_ ( x)

这个证明的结构于扩充的结构相似。 新加入的变量平凡地与它自身相关,否则我们可以对假设使用重命名。

如上定义完扩充之后,证明替换与模拟交换就很直接了。 如果 σσ† 都将任何判断 Γ ∋ A 映射至一个判断 Δ ⊢ A, 且对于 Γ ∋ A 中的任意 x,我们有 σ x ~ σ† x, 如果 M ~ M†,那么 subst σ M ~ subst σ† M†

~subst :  {Γ Δ}
   {σ  :  {A}  Γ  A  Δ  A}
   {σ† :  {A}  Γ  A  Δ  A}
   (∀ {A}  (x : Γ  A)  σ x ~ σ† x)
    ---------------------------------------------------------
   (∀ {A} {M M† : Γ  A}  M ~ M†  subst σ M ~ subst σ† M†)
~subst  (~` {x = x})  =   x
~subst  ( ~N)       =   (~subst (~exts ) ~N)
~subst  (~L  ~M)    =  (~subst  ~L)  (~subst  ~M)
~subst  (~let ~M ~N)  =  ~let (~subst  ~M) (~subst (~exts ) ~N)

与之前一样,这个证明的结构于替换的结构类似:使用递归重新构造每一个项, 并在需要时扩充语境(在这里,我们只需要在 λ 抽象的抽象体中使用)。

从上面的替换的广义定义,我们可以简单地推导出我们所需的特殊形式: 如果 N ~ N†M ∼ M†,那么 N [ M ] ~ N† [ M† ]

~sub :  {Γ A B} {N N† : Γ , B  A} {M M† : Γ  B}
   N ~ N†
   M ~ M†
    -----------------------
   (N [ M ]) ~ (N† [ M† ])
~sub {Γ} {A} {B} ~N ~M = ~subst {Γ , B} {Γ}  {A} ~N
  where
   :  {A}  (x : Γ , B  A)  _ ~ _
   Z      =  ~M
   (S x)  =  ~`

再一次,这个证明与原定义的结构类似。

给出的关系是模拟

最后,我们可以证明给出的关系的确是一个模拟。 实际上,我们将展示它满足更强的锁步模拟的条件。 我们期望展示的有:

锁步模拟:对于任意的 MM†N, 如果 M ~ M†M —→ N 成立,那么 M† —→ N†N ~ N† 对于一些 N† 成立。

或者,用图表来表示:

M  --- —→ --- N
|             |
|             |
~             ~
|             |
|             |
M† --- —→ --- N†

我们首先形式化对应图表下方的一段的一个概念,即右面和下面的边:

data Leg {Γ A} (M† N : Γ  A) : Set where

  leg :  {N† : Γ  A}
     N ~ N†
     M† —→ N†
      --------
     Leg M† N

在我们的形式化中,在这里,我们可以使用比 —↠ 更强的关系,用 —→ 来取而代之。

我们现在可以陈述并证明这个关系是一个模拟。 同样,在这里,我们可以使用比 —↠ 更强的关系,用 —→ 来取而代之。

sim :  {Γ A} {M M† N : Γ  A}
   M ~ M†
   M —→ N
    ---------
   Leg  M† N
sim ~`              ()
sim ( ~N)         ()
sim (~L  ~M)      (ξ-·₁ L—→)
  with sim ~L L—→
...  | leg ~L′ L†—→                 =  leg (~L′  ~M)   (ξ-·₁ L†—→)
sim (~V  ~M)      (ξ-·₂ VV M—→)
  with sim ~M M—→
...  | leg ~M′ M†—→                 =  leg (~V  ~M′)   (ξ-·₂ (~val ~V VV) M†—→)
sim (( ~N)  ~V) (β-ƛ VV)        =  leg (~sub ~N ~V)  (β-ƛ (~val ~V VV))
sim (~let ~M ~N)    (ξ-let M—→)
  with sim ~M M—→
...  | leg ~M′ M†—→                 =  leg (~let ~M′ ~N) (ξ-·₂ V-ƛ M†—→)
sim (~let ~V ~N)    (β-let VV)      =  leg (~sub ~N ~V)  (β-ƛ (~val ~V VV))

这个证明由分情况讨论来完成,检查每一个 M ~ M† 的例子,和每一个 M —→ M† 的例子, 并在规约是 ξ 规则时使用递归,也因此包括了另一个规约。 证明的结构和可进性的证明也有些类似:

  • 如果相关的项是变量,那么无可应用的规约。
  • 如果相关的项是 λ 抽象,那么无可应用的规约。
  • 如果相关的项是应用,那么有三种子情况:
    • 源项由 ξ-·₁ 规约,在此情况下目标项也一样。递归应用可以让我们得到:

      L  --- —→ ---  L′
      |              |
      |              |
      ~              ~
      |              |
      |              |
      L† --- —→ --- L′†

      从而得到:

       L · M  --- —→ ---  L′ · M
         |                   |
         |                   |
         ~                   ~
         |                   |
         |                   |
      L† · M† --- —→ --- L′† · M†
    • 源项由 ξ-·₂ 规约,在此情况下目标项也一样。递归应用可以让我们得到:

      M  --- —→ ---  M′
      |              |
      |              |
      ~              ~
      |              |
      |              |
      M† --- —→ --- M′†

      从而得到:

       V · M  --- —→ ---  V · M′
         |                  |
         |                  |
         ~                  ~
         |                  |
         |                  |
      V† · M† --- —→ --- V† · M′†

      因为模拟和值可交换,且 V 是一个值,所以 V† 也是一个值。

    • 源项由 β-ƛ 规约,在此情况下目标项也一样:

       (ƛ x ⇒ N) · V  --- —→ ---  N [ x := V ]
            |                           |
            |                           |
            ~                           ~
            |                           |
            |                           |
      (ƛ x ⇒ N†) · V† --- —→ --- N† [ x :=  V† ]

      因为模拟和值可交换,且 V 是一个值,所以 V† 也是一个值。 因为模拟和替换可交换,且 N ~ N†V ~ V† 成立,所以我们得到 N [ x := V ] ~ N† [ x := V† ]

  • 如果相关的项是 let 和抽象应用,那么有两种子情况:

    • 源项由 ξ-let 规约,在此情况下目标项由 ξ-·₂ 规约。递归应用可以让我们得到:

      M  --- —→ ---  M′
      |              |
      |              |
      ~              ~
      |              |
      |              |
      M† --- —→ --- M′†

      从而得到:

      let x = M in N --- —→ --- let x = M′ in N
            |                         |
            |                         |
            ~                         ~
            |                         |
            |                         |
      (ƛ x ⇒ N) · M  --- —→ --- (ƛ x ⇒ N) · M′
    • 源项由 β-let 规约,在此情况下目标项由 β-ƛ 规约:

      let x = V in N  --- —→ ---  N [ x := V ]
            |                         |
            |                         |
            ~                         ~
            |                         |
            |                         |
      (ƛ x ⇒ N†) · V† --- —→ --- N† [ x := V† ]

      因为模拟和值可交换,且 V 是一个值,所以 V† 也是一个值。 因为模拟和替换可交换,且 N ~ N†V ~ V† 成立,所以我们得到 N [ x := V ] ~ N† [ x := V† ]

练习 sim⁻¹ (实践)

证明我们也有反方向的模拟,因此我们有一个互模拟。

-- 在这里写出你的代码。
练习 products (实践)

证明 More 章节中两种积的表达方式满足互模拟。 你需要包含的构造有:变量,以及与函数和积相关的构造。 在此情况下,模拟并不是锁步的。

-- 在这里写出你的代码。

Unicode

本章节使用了下列 Unicode:

†  U+2020  DAGGER (\dag)
⁻  U+207B  SUPERSCRIPT MINUS (\^-)
¹  U+00B9  SUPERSCRIPT ONE (\^1)

Inference: 双向类型推理

module plfa.part2.Inference where

在本书至此的进展中,项的类型推导是如法令一般直接给出的。 在 Lambda 章节,类型推导以外在于项的形式给出, 而在 DeBruijn 章节,类型推导以内在于项的形式给出, 但在两者均要求我们将类型推导完全写出。

在实践中,我们一般可以给项加上一些装饰,然后运用算法来推理(Infer)出类型推导。 的确,Agda 中也是这样:我们给顶层的函数声明指定类型,而其余可由给出的信息推理而来。 Agda 使用的这种推理被称为双向(Bidirectional)类型推理,我们将在本章中进行展示。

本章中,我们讲之前的进展结合在一起。 我们首先由带有类型注释的项开始,其与 Lambda 章节中的源项相似, 从此我们计算出内在类型的项,如同 DeBruijn 章节中那样。

绪论:推理规则作为算法

在我们至此使用的演算中,一个项可以有多于一个类型。例如,

(ƛ x ⇒ x) ⦂ (A ⇒ A)

对于任意类型 A 成立。 我们首先考虑一个带有 λ 项的小语言,其每一项有其唯一的类型。 我们只需要给抽象加上参数的类型。 这样我们可以得到如下的语法:

L, M, N ::=                         带装饰的项
  x                                   变量
  ƛ x ⦂ A ⇒ N                         抽象(装饰后)
  L · M                               应用

每一条相关的赋型规则可以被读作类型检查的算法。 对于每一条赋型规则,我们将每个位置标记如输入(Input)或者输出(Output)。

对于下面的判断

Γ ∋ x ⦂ A

我们将语境 Γ 和变量 x 作为输入,类型 A 作为输出。 考虑下面的规则:

----------------- Z
Γ , x ⦂ A ∋ x ⦂ A

Γ ∋ x ⦂ A
----------------- S
Γ , y ⦂ B ∋ x ⦂ A

从输入中,我们可以决定应用哪一条规则: 如果语境中最后一个变量与给定的变量一致,那么应用第一条规则,否则应用第二条。 (对于 de Bruijn 因子来说,这更加简单:零对应第一条,后继对应第二条。) 对于第一条,输出类型可以直接从语境中得到。 对于第二条,结论中的输入可以作为假设的输入,而假设的输出决定了结论的输出。

对于下面的判断:

Γ ⊢ M ⦂ A

我们将语境 Γ 和项 M 作为输入,类型 A 作为输出。 考虑下面的规则:

Γ ∋ x ⦂ A
-----------
Γ ⊢ ` x ⦂ A

Γ , x ⦂ A ⊢ N ⦂ B
---------------------------
Γ ⊢ (ƛ x ⦂ A ⇒ N) ⦂ (A ⇒ B)

Γ ⊢ L ⦂ A ⇒ B
Γ ⊢ M ⦂ A′
A ≡ A′
-------------
Γ ⊢ L · M ⦂ B

输入项决定了应用哪一条规则: 变量使用第一条,抽象使用第二条,应用使用第三条。 我们把这样的规则叫做语法导向的(Syntax directed)规则。 对于变量的规则,结论的输入决定了结论的输出。 抽象的规则也是一样——约束变量和参数从结论的输入流向假设中的语境; 这得以实现,因为我们在抽象中加入了参数的类型。 对于应用的规则,我们加入第三条假设来检查函数类型中的作用域是否与参数的类型一致; 这条判断是在两个类型已知时是可判定的。 结论的输入决定了前两个假设的输入,而前两个假设的输出决定了第三个假设的输入,而第一个假设的输出决定了结论的输出。

我们可以直接地把上述内容转换成一个算法,加入自然数和不动点也很直接。我们省略其明细。 取而代之的是,我们考虑一种需要更少装饰的表示方法。 其核心思想是将普通的赋型判断拆分成两个判断,一个生成类型作为输出,另一个取类型作为输入。

生成和继承类型

我们保留之前的变量的查询判断。除此之外,我们有两种联系类型和项的判断:

Γ ⊢ M ↑ A
Γ ⊢ M ↓ A

第一类判断生成(Synthesise)项的类型,如上,而第二类继承(Inherit)类型。 在第一类中,语境和项作为输入,类型作为输出; 而在第二类中,语境、项和类型三者都作为输入。

什么项生成类型,什么项继承类型的? 我们的宗旨是解构子中的主项使用生成来赋型,而构造子使用继承。 比如,函数应用中的函数由生成来赋型,而抽象是由继承来赋型。 抽象中继承的类型和之前我们为参数额外增加的注解起到一样的作用。

解构某一类型的值的项总有一个主项(提供一个所需类型的参数),且经常有副项。 对于函数应用来说,主项提供了函数,副项提供了参数。 对于分情况讨论来说,主项提供了自然数,副项则是两种情况不同的情况。 在解构子中,主项使用生成进行赋型,而副项使用继承进行赋型。 我们将看到,这自然地导致函数应用作为整体由生成进行赋型,而分情况讨论作为整体则使用继承进行赋型。 变量一般使用生成进行赋型,因为我们可以直接从语境中查询其类型。 不动点自然是用继承来赋型。

为了达成赋型系统的语法导向性,我们将项分为两类: Term⁺Term⁻,分别用生成和继承来赋型。 由生成赋型的子项可能出现在由继承赋型的子项中,反之亦然,这给我们带来两种新形式。

例如,我们之前提到函数应用的参数由继承赋型,而变量由生成赋型。 在参数是变量时会出现不匹配的情况。 因此,我们需要一种把生成的项当作继承的项的方法。 我们引入 M ↑ 这种新的形式来达成此目的。 它的赋型判断即为检查其生成和继承的类型是否一致。

同样,函数应用的函数部分由生成来赋型,而抽象是由继承来赋型。 在函数是抽象时会出现不匹配的情况。 因此,我们需要一种把继承的项当作生成的项的方法。 我们引入 M ↓ A 这种新的形式来达成此目的。 它的赋型判断返回 A 作为生成的类型,并把它作为 M 继承的类型。

M ↓ A 这种形式表示了我们唯一需要用类型来装饰的项。 它只在从生成切换成继承时出现, 即当一个解构某一类型的值的项的主项中包含了一个构造某一类型的值的时候, 也就是说,这是在 β 规约发生的地方出现。 一般来说,我们只需要在顶层声明中需要这样的装饰。

我们可以从上文中提取出项的语法:

L⁺, M⁺, N⁺ ::=                      带有生成类型的项
  x                                   变量
  L⁺ · M⁻                             应用
  M⁻ ↓ A                              切换至继承

L⁻, M⁻, N⁻ ::=                      带有继承类型的项
  ƛ x ⇒ N⁻                            抽象
  `zero                               零
  `suc M⁻                             后继
  case L⁺ [zero⇒ M⁻ |suc x ⇒ N⁻ ]     分情况讨论
  μ x ⇒ N⁻                            不动点
  M⁺ ↑                                切换至生成

我们将在下文中形式化上述定义。

可靠性和完备性

我们试图证明赋型判断是可判定的:

synthesize : ∀ (Γ : Context) (M : Term⁺)
    ------------------------------------
  → Dec (∃[ A ] Γ ⊢ M ↑ A)

inherit : ∀ (Γ : Context) (M : Term⁻) (A : Type)
          --------------------------------------
        → Dec (Γ ⊢ M ↓ A)

给定语境 Γ 和生成项 M,我们必须判定是否存在一个类型 A 使得 Γ ⊢ M ↑ A 成立, 或者其否定。 同样,给定语境 Γ 、继承项 M 和类型 A,我们必须判定 Γ ⊢ M ↓ A 成立,或者其否定。

我们的证明是构造性的。 在生成的情况中,它要么给出一个包括类型 AΓ ⊢ M ↓ A 成立的证明的有序对,或是一个将上述有序对转换成矛盾的函数。 在继承的情况中,它要么给出 Γ ⊢ M ↑ A 成立的证明,或是一个将上述证明转换成矛盾的函数。 成立的情况被称为可靠性(Soundness)——生成和继承只在对应关系成立时成功。 不成立的情况被称为完备性(Completeness)——生成和继承只在对应关系无法成立时失败。

另一种实现方法可能在生成或继承成功时返回其推导,并在失败时返回一条错误信息——例如, 参见 Agda 用户手册中讨论语法糖的章节。 这样的方法可以实现可靠性,而不是完备性。 如果其返回一个推导,那么我们知道它是正确的; 但没有人阻挡我们来给出一个总是返回错误的函数,即使正确的推导存在。 证明可靠性和完备性两者比起单独证明可靠性一者更加有力。 其反向的证明可以被看作是一个语义上验证过的错误信息,即便它本身比仔细撰写的错误信息更让人难懂。

我们现在可以开始正式的形式化了:

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl; sym; trans; cong; cong₂; _≢_)
open import Data.Empty using (; ⊥-elim)
open import Data.Nat using (; zero; suc; _+_; _*_)
open import Data.String using (String; _≟_)
open import Data.Product using (_×_; ; ∃-syntax) renaming (_,_ to ⟨_,_⟩)
open import Relation.Nullary using (¬_; Dec; yes; no)
open import Relation.Nullary.Decidable using (False; toWitnessFalse)

当我们有一个赋型推导时,从它构造出内在类型的表示方法更加方便。 为了与我们之前的结果进行比较,我们导入模块 plfa.part2.More

import plfa.part2.More as DB

as DB 这个词组让我们可以用例如 DB._⊢_ 的方法来指代其中的定义, 我们用 Γ DB.⊢ A 来使用它,其中 Γ 的类型是 DB.ContextA 的类型是 DB.Type

语法

首先,我们来定义中缀声明。 我们将判断和项的运算符分别列出:

infix   4  _∋_⦂_
infix   4  _⊢_↑_
infix   4  _⊢_↓_
infixl  5  _,_⦂_

infixr  7  _⇒_

infix   5  ƛ_⇒_
infix   5  μ_⇒_
infix   6  _↑
infix   6  _↓_
infixl  7  _·_
infix   8  `suc_
infix   9  `_

标识符、类型和语境与之前一样:

Id : Set
Id = String

data Type : Set where
  `ℕ    : Type
  _⇒_   : Type  Type  Type

data Context : Set where
       : Context
  _,_⦂_ : Context  Id  Type  Context

项的语法由共同递归来定义。 我们用 Term⁺Term⁻ 来分别表示生成和继承的项。 注意我们包括了变向的形式 M ↓ AM ↑

data Term⁺ : Set
data Term⁻ : Set

data Term⁺ where
  `_                       : Id  Term⁺
  _·_                      : Term⁺  Term⁻  Term⁺
  _↓_                      : Term⁻  Type  Term⁺

data Term⁻ where
  ƛ_⇒_                     : Id  Term⁻  Term⁻
  `zero                    : Term⁻
  `suc_                    : Term⁻  Term⁻
  `case_[zero⇒_|suc_⇒_]    : Term⁺  Term⁻  Id  Term⁻  Term⁻
  μ_⇒_                     : Id  Term⁻  Term⁻
  _↑                       : Term⁺  Term⁻

至于每个项是由继承或者生成来赋型,我们在上文中已经讨论过,并且可以直接上文的非形式化语法中直接得来。 解构子中的主项由生成赋型,解构子中的构造子和副项由继承赋型。

项的例子

我们重新给出前几章中的例子。 首先,计算自然数二加二:

two : Term⁻
two = `suc (`suc `zero)

plus : Term⁺
plus = (μ "p"  ƛ "m"  ƛ "n" 
          `case (` "m") [zero⇒ ` "n" 
                        |suc "m"  `suc (` "p" · (` "m" ) · (` "n" ) ) ])
             (`ℕ  `ℕ  `ℕ)

2+2 : Term⁺
2+2 = plus · two · two

唯一的变化是我们需要在需要时加入上下箭头。唯一的类型注释出现在 plus

接下来,计算 Church 数的二加二:

Ch : Type
Ch = (`ℕ  `ℕ)  `ℕ  `ℕ

twoᶜ : Term⁻
twoᶜ = (ƛ "s"  ƛ "z"  ` "s" · (` "s" · (` "z" ) ) )

plusᶜ : Term⁺
plusᶜ = (ƛ "m"  ƛ "n"  ƛ "s"  ƛ "z" 
           ` "m" · (` "s" ) · (` "n" · (` "s" ) · (` "z" ) ) )
              (Ch  Ch  Ch)

sucᶜ : Term⁻
sucᶜ = ƛ "x"  `suc (` "x" )

2+2ᶜ : Term⁺
2+2ᶜ = plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero

唯一的类型注释出现在 plusᶜsucᶜ 甚至不需要类型注释,因为它从 plusᶜ 的参数中继承了类型。

双向类型检查

变量的赋型规则与 Lambda 章节中一致:

data _∋_⦂_ : Context  Id  Type  Set where

  Z :  {Γ x A}
      -----------------
     Γ , x  A  x  A

  S :  {Γ x y A B}
     x  y
     Γ  x  A
      -----------------
     Γ , y  B  x  A

与语法一样,生成和继承的赋型判断也是共同递归的:

data _⊢_↑_ : Context  Term⁺  Type  Set
data _⊢_↓_ : Context  Term⁻  Type  Set

data _⊢_↑_ where

  ⊢` :  {Γ A x}
     Γ  x  A
      -----------
     Γ  ` x  A

  _·_ :  {Γ L M A B}
     Γ  L  A  B
     Γ  M  A
      -------------
     Γ  L · M  B

  ⊢↓ :  {Γ M A}
     Γ  M  A
      ---------------
     Γ  (M  A)  A

data _⊢_↓_ where

  ⊢ƛ :  {Γ x N A B}
     Γ , x  A  N  B
      -------------------
     Γ  ƛ x  N  A  B

  ⊢zero :  {Γ}
      --------------
     Γ  `zero  `ℕ

  ⊢suc :  {Γ M}
     Γ  M  `ℕ
      ---------------
     Γ  `suc M  `ℕ

  ⊢case :  {Γ L M x N A}
     Γ  L  `ℕ
     Γ  M  A
     Γ , x  `ℕ  N  A
      -------------------------------------
     Γ  `case L [zero⇒ M |suc x  N ]  A

  ⊢μ :  {Γ x N A}
     Γ , x  A  N  A
      -----------------
     Γ  μ x  N  A

  ⊢↑ :  {Γ M A B}
     Γ  M  A
     A  B
      -------------
     Γ  (M )  B

我们用和 Lambda 中一样的命名规则, 把 加在构造子之前,来当作对应赋型规则的名称。

这些规则和 Lambda 章节中相似,修改成支持生成和继承类型的样式。 其中有两条新规则 ⊢↓⊢↑。 前者把类型装饰当作继承的类型,并将其返回作为生成的类型。 后者取其生成的类型,并检查是否与继承的类型一致——这应该让你回忆起第一部分中函数应用规则的相等性测试。

练习 bidirectional-mul (推荐)

将你在 Lambda 章节中乘法的定义重写,将其装饰至支持类型推理的形式。

-- 请将代码写在此处。
练习 bidirectional-products (推荐)

扩充你的双向赋型规则,来包括 More 章节中的积。

-- 请将代码写在此处。
练习 bidirectional-rest (延伸)

扩充你的双向赋型规则,来包括 More 章节中的其余构造。

-- 请将代码写在此处。

前置需求

M ↑ 规则需要我们有能力来判定两个类型是否相等。 我们可以直接地给出代码:

_≟Tp_ : (A B : Type)  Dec (A  B)
`ℕ      ≟Tp `ℕ              =  yes refl
`ℕ      ≟Tp (A  B)         =  no λ()
(A  B) ≟Tp `ℕ              =  no λ()
(A  B) ≟Tp (A′  B′)
  with A ≟Tp A′ | B ≟Tp B′
...  | no A≢    | _         =  no λ{refl  A≢ refl}
...  | yes _    | no B≢     =  no λ{refl  B≢ refl}
...  | yes refl | yes refl  =  yes refl

我们也会需要一些显然的引理; 作用域和值域相等的函数类型相等:

dom≡ :  {A A′ B B′}  A  B  A′  B′  A  A′
dom≡ refl = refl

rng≡ :  {A A′ B B′}  A  B  A′  B′  B  B′
rng≡ refl = refl

我们也需要知道 `ℕA ⇒ B 这两个类型不相等:

ℕ≢⇒ :  {A B}  `ℕ  A  B
ℕ≢⇒ ()

唯一的类型

从语境查询一个类型是唯一的。 给定两个推导,其一证明 Γ ∋ x ⦂ A,另一个证明 Γ ∋ x ⦂ B,那么 AB 一定是一样的:

uniq-∋ :  {Γ x A B}  Γ  x  A  Γ  x  B  A  B
uniq-∋ Z Z                 =  refl
uniq-∋ Z (S x≢y _)         =  ⊥-elim (x≢y refl)
uniq-∋ (S x≢y _) Z         =  ⊥-elim (x≢y refl)
uniq-∋ (S _ ∋x) (S _ ∋x′)  =  uniq-∋ ∋x ∋x′

如果两个推导都使用 Z 规则,那么唯一性即可直接得出; 如果两个推导都使用 S 规则,那么唯一性可以由归纳得出。 如果一个使用了 Z 规则,另一个使用了 S 规则,这则是一个矛盾, 因为 Z 要求我们查询的变量是语境中的最后一个,而 S 要求不是这样。

生成的类型也是唯一的。 给定两个推导,其一证明 Γ ⊢ M ↑ A,另一个证明 Γ ⊢ M ↑ B,那么 AB 一定是一样的:

uniq-↑ :  {Γ M A B}  Γ  M  A  Γ  M  B  A  B
uniq-↑ (⊢` ∋x) (⊢` ∋x′)       =  uniq-∋ ∋x ∋x′
uniq-↑ (⊢L · ⊢M) (⊢L′ · ⊢M′)  =  rng≡ (uniq-↑ ⊢L ⊢L′)
uniq-↑ (⊢↓ ⊢M) (⊢↓ ⊢M′)       =  refl

有三种项的形式。 如果项是变量,那么生成的唯一性从查询的唯一性中直接得出。 如果项是函数应用,那么唯一性由应用中的函数之上的归纳得出,因为值域相等的类型相等。 如果项是变向,两者装饰的类型相等,从此可得唯一性。

查询语境中变量的类型

给定 Γ 和两个不同的变量 xy, 如果不存在类型 A 使得 Γ ∋ x ⦂ A 成立, 那么也不存在类型 A 使得 Γ , y ⦂ B ∋ x ⦂ A 成立:

ext∋ :  {Γ B x y}
   x  y
   ¬ (∃[ A ] Γ  x  A)
    ----------------------------
   ¬ (∃[ A ] Γ , y  B  x  A)
ext∋ x≢y _   A , Z        =  x≢y refl
ext∋ _   ¬∃  A , S _ ∋x   =  ¬∃  A , ∋x 

给定类型 AΓ , y ⦂ B ∋ x ⦂ A 成立的证明,我们必须构造一个矛盾。 如果判断由 Z 成立,那么 xy 一定是一样的,与第一个假设矛盾。 如果判断由 S _ ∋x 成立,那么 ∋x 提供了 Γ ∋ x ⦂ A 成立的证明,与第二个假设矛盾。

给定语境 Γ 和变量 x,我们可判断是否存在一个类型 A 使得 Γ ∋ x ⦂ A 成立,或者其反命题:

lookup :  (Γ : Context) (x : Id)
         ------------------------
        Dec (∃[ A ] Γ  x  A)
lookup  x                        =  no   ())
lookup (Γ , y  B) x with x  y
... | yes refl                    =  yes  B , Z 
... | no x≢y with lookup Γ x
...             | no  ¬∃          =  no  (ext∋ x≢y ¬∃)
...             | yes  A , ∋x   =  yes  A , S x≢y ∋x 

考虑语境的情况:

  • 如果语境为空,那么平凡地,我们没有任何可能的推导。
  • 如果语境非空,比较给定的变量和最新的绑定:

    • 如果它们一致,我们完成了判定,使用 Z 作为对应的推导。
    • 如果它们不一致,我们递归:

      • 如果查询失败了,我们使用 ext∋ 将变量不存在于内部的语境中的证明扩充至扩充后的语境。
      • 如果查询成功了,我们对返回的推导使用 S

提升否定

对于每一个可能的项的形式,我们需要证明:如果其内部的子项无法被赋型,那么整个项无法被赋型。 大多数情况下,我们可以直接在行中直接完成所需的证明,但有一些困难的情况, 我们提供一些帮助函数。

如果 Γ ⊢ L ↑ A ⇒ B 成立而 Γ ⊢ M ↓ A 不成立,那么不存在使得 Γ ⊢ L · M ↑ B′ 成立的类型 B′

¬arg :  {Γ A B L M}
   Γ  L  A  B
   ¬ Γ  M  A
    --------------------------
   ¬ (∃[ B′ ] Γ  L · M  B′)
¬arg ⊢L ¬⊢M  B′ , ⊢L′ · ⊢M′  rewrite dom≡ (uniq-↑ ⊢L ⊢L′) = ¬⊢M ⊢M′

⊢L 作为 Γ ⊢ L ↑ A ⇒ B 成立的证明、 ¬⊢M 作为 Γ ⊢ M ↓ A 不成立的证明。 给定类型 B′Γ ⊢ L · M ↑ B′ 成立的证明,我们必须构造一个矛盾。 这样的证明一定是 ⊢L′ · ⊢M′ 的形式,其中 ⊢L′Γ ⊢ L ↑ A′ ⇒ B′ 成立的证明、⊢M′Γ ⊢ M ↓ A′ 成立的证明。 将 uniq-↑ 应用于 ⊢L⊢L′,我们知道 A ⇒ B ≡ A′ ⇒ B′, 所以可得 A ≡ A′,这意味着 ¬⊢M⊢M′ 可以构造出一个矛盾。 不使用 rewrite 语句的话,Agda 不会让我们从 ¬⊢M⊢M′ 中构造出一个矛盾, 因为其中的类型分别是 AA′

如果 Γ ⊢ M ↑ A 成立且 A ≢ B,那么 Γ ⊢ (M ↑) ↓ B 不成立:

¬switch :  {Γ M A B}
   Γ  M  A
   A  B
    ---------------
   ¬ Γ  (M )  B
¬switch ⊢M A≢B (⊢↑ ⊢M′ A′≡B) rewrite uniq-↑ ⊢M ⊢M′ = A≢B A′≡B

⊢M 作为 Γ ⊢ M ↑ A 成立的证明、A≢B 作为 A ≢ B 成立的证明。 给定 Γ ⊢ (M ↑) ↓ B 成立的证明,我们必须构造一个矛盾。 这样的证明一定是 ⊢↑ ⊢M′ A′≡B 的形式,其中 ⊢M′Γ ⊢ M ↑ A′ 成立的证明、A′≡BA′ ≡ B 成立的证明。 将 uniq-↑ 应用于 ⊢M⊢M′,我们知道 A ≡ A′,这意味着 A≢BA′≡B 可以构造出一个矛盾。 不使用 rewrite 语句的话,Agda 不会让我们从 A≢BA′≡B 中构造出一个矛盾, 因为其中的类型分别是 AA′

生成和继承类型

餐桌已经布置好了,我们已经准备好享用今天的主菜了。 我们定义两个共同递归的函数,一个用于生成,一个用于继承。 生成在给定语境 Γ 和生成项 M 时,要么返回一个类型 AΓ ⊢ M ↑ A 成立的证明,或者其/否定。 继承在给定语境 Γ 、继承项 M 和类型 A 时要么返回 Γ ⊢ M ↓ A 成立的证明,或者其否定:

synthesize :  (Γ : Context) (M : Term⁺)
             ---------------------------
            Dec (∃[ A ] Γ  M  A )

inherit :  (Γ : Context) (M : Term⁻) (A : Type)
    ---------------
   Dec (Γ  M  A)

我们首先考虑生成的代码:

synthesize Γ (` x) with lookup Γ x
... | no  ¬∃              =  no  (λ{  A , ⊢` ∋x   ¬∃  A , ∋x  })
... | yes  A , ∋x       =  yes  A , ⊢` ∋x 
synthesize Γ (L · M) with synthesize Γ L
... | no  ¬∃              =  no  (λ{  _ , ⊢L  · _      ¬∃  _ , ⊢L  })
... | yes  `ℕ ,    ⊢L   =  no  (λ{  _ , ⊢L′ · _      ℕ≢⇒ (uniq-↑ ⊢L ⊢L′) })
... | yes  A  B , ⊢L  with inherit Γ M A
...    | no  ¬⊢M          =  no  (¬arg ⊢L ¬⊢M)
...    | yes ⊢M           =  yes  B , ⊢L · ⊢M 
synthesize Γ (M  A) with inherit Γ M A
... | no  ¬⊢M             =  no  (λ{  _ , ⊢↓ ⊢M     ¬⊢M ⊢M })
... | yes ⊢M              =  yes  A , ⊢↓ ⊢M 

有三种情况来考虑:

  • 如果项是变量 ` x,我们使用之前定义的查询:

    • 如果失败了,那么 ¬∃ 是不存在使得 Γ ∋ x ⦂ A 成立的类型 A 的证明。 Γ ⊢ ` x ↑ A 成立的证明一定是 ⊢` ∋x 的形式,其中 ∋xΓ ∋ x ⦂ A 成立的证明,这构成了一个矛盾。
    • 如果成功了,那么 ∋xΓ ∋ x ⦂ A 成立的证明,所以 ⊢` ∋xΓ ⊢ ` x ↑ A 成立的证明。
  • 如果项是函数应用 L · M,我们递归于函数项 L

    • 如果失败了,那么 ¬∃ 是不存在任何类型 Γ ⊢ L ↑ _ 成立的证明。 Γ ⊢ L · M ↑ _ 成立的证明一定是 ⊢L · _ 的形式,其中 ⊢LΓ ⊢ L ↑ _ 成立的证明,这构成了一个矛盾。
    • 如果成功了,那么有两种情况:

      • 其一是 ⊢LΓ ⊢ L ⦂ `ℕ 成立的证明。 Γ ⊢ L · M ↑ _ 成立的证明一定是 ⊢L′ · _ 的形式,其中 ⊢L′Γ ⊢ L ↑ A ⇒ B 对于一些类型 AB 成立的证明。 将 uniq-↑ 应用于 ⊢L⊢L′ 构成了一个矛盾, 因为 `ℕ 无法与 A ⇒ B 相等。
      • 另一是 ⊢LΓ ⊢ L ↑ A ⇒ B 成立的证明,那么我们在参数项 M 上递归:

        • 如果失败了,那么 ¬⊢MΓ ⊢ M ↓ A 不成立的证明。 将 ¬arg 应用于 ⊢L¬⊢M,我们可得 Γ ⊢ L · M ↑ B 不成立。
        • 如果成功了,那么 ⊢MΓ ⊢ M ↓ A 成立的证明, 且 ⊢L · ⊢M 提供了 Γ ⊢ L · M ↑ B 成立的证明。
  • 如果项是从生成到继承的变向项 M ↓ A,我们在子项 M 上递归,并向继承提供类型 A

    • 如果失败了,那么 ¬⊢MΓ ⊢ M ↓ A 不成立的证明。 Γ ⊢ (M ↓ A) ↑ A 成立的证明一定是 ⊢↓ ⊢M 的形式,其中 ⊢MΓ ⊢ M ↓ A 成立的证明,这构成了一个矛盾。
    • 如果成功了,那么 ⊢MΓ ⊢ M ↓ A 成立的证明,且 ⊢↓ ⊢M 提供了 Γ ⊢ (M ↓ A) ↑ A 成立的证明。

我们接下来考虑继承的代码:

inherit Γ (ƛ x  N) `ℕ      =  no  (λ())
inherit Γ (ƛ x  N) (A  B) with inherit (Γ , x  A) N B
... | no ¬⊢N                =  no  (λ{ (⊢ƛ ⊢N)    ¬⊢N ⊢N })
... | yes ⊢N                =  yes (⊢ƛ ⊢N)
inherit Γ `zero `ℕ          =  yes ⊢zero
inherit Γ `zero (A  B)     =  no  (λ())
inherit Γ (`suc M) `ℕ with inherit Γ M `ℕ
... | no ¬⊢M                =  no  (λ{ (⊢suc ⊢M)    ¬⊢M ⊢M })
... | yes ⊢M                =  yes (⊢suc ⊢M)
inherit Γ (`suc M) (A  B)  =  no  (λ())
inherit Γ (`case L [zero⇒ M |suc x  N ]) A with synthesize Γ L
... | no ¬∃                 =  no  (λ{ (⊢case ⊢L  _ _)  ¬∃  `ℕ , ⊢L })
... | yes  _  _ , ⊢L     =  no  (λ{ (⊢case ⊢L′ _ _)  ℕ≢⇒ (uniq-↑ ⊢L′ ⊢L) })
... | yes  `ℕ ,    ⊢L  with inherit Γ M A
...    | no ¬⊢M             =  no  (λ{ (⊢case _ ⊢M _)  ¬⊢M ⊢M })
...    | yes ⊢M with inherit (Γ , x  `ℕ) N A
...       | no ¬⊢N          =  no  (λ{ (⊢case _ _ ⊢N)  ¬⊢N ⊢N })
...       | yes ⊢N          =  yes (⊢case ⊢L ⊢M ⊢N)
inherit Γ (μ x  N) A with inherit (Γ , x  A) N A
... | no ¬⊢N                =  no  (λ{ (⊢μ ⊢N)  ¬⊢N ⊢N })
... | yes ⊢N                =  yes (⊢μ ⊢N)
inherit Γ (M ) B with synthesize Γ M
... | no  ¬∃                =  no  (λ{ (⊢↑ ⊢M _)  ¬∃  _ , ⊢M  })
... | yes  A , ⊢M  with A ≟Tp B
...   | no  A≢B             =  no  (¬switch ⊢M A≢B)
...   | yes A≡B             =  yes (⊢↑ ⊢M A≡B)

我们在此只考虑抽象和从继承变向至生成的情况:

  • 如果项是 ƛ x ⇒ N,而继承的类型是 `ℕ,那么平凡地, Γ ⊢ (ƛ x ⇒ N) ↓ `ℕ 无法成立。
  • 如果项是 ƛ x ⇒ N,而继承的类型是 A ⇒ B,我们用语境 Γ , x ⦂ A 递归至 子项 N 继承类型 B

    • 如果失败了,那么 ¬⊢NΓ , x ⦂ A ⊢ N ↓ B 不成立的证明。 Γ ⊢ (ƛ x ⇒ N) ↓ A ⇒ B 成立的证明一定是 ⊢ƛ ⊢N 的形式,其中 ⊢NΓ , x ⦂ A ⊢ N ↓ B 成立的证明,这构成了一个矛盾。
    • 如果成功了,那么 ⊢NΓ , x ⦂ A ⊢ N ↓ B 成立的证明,且 ⊢ƛ ⊢N 提供了 Γ ⊢ (ƛ x ⇒ N) ↓ A ⇒ B 成立的证明。
  • 如果项是从继承到生成的变向项 M ↑,我们在子项 M 上递归:

    • 如果失败了,那么 ¬∃ 是不存在使得 Γ ⊢ M ↑ A 成立的类型 A 的证明。 Γ ⊢ (M ↑) ↓ B 成立的证明一定是 ⊢↑ ⊢M _ 的形式,其中 ⊢MΓ ⊢ M ↑ _ 成立的证明,这构成了一个矛盾。
    • 如果成功了,那么 ⊢MΓ ⊢ M ↑ A 成立的证明。 我们应用 _≟Tp_ 来判定 AB 是否相等:

      • 如果失败了,那么 A≢BA ≢ B 的证明。 将 ¬switch 应用于 ⊢MA≢B,我们可得 Γ ⊢ (M ↑) ↓ B 无法成立。
      • 如果成功了,那么 A≡BA ≡ B 的证明, 且 ⊢↑ ⊢M A≡B 提供了 Γ ⊢ (M ↑) ↓ B 成立的证明。

剩余的情况类似,它们的代码可以由对应的赋型规则直接对应得来。

测试项的例子

首先,我们复制之前介绍过的智能构造子 S′,使得访问语境中的变量更加便利:

S′ :  {Γ x y A B}
    {x≢y : False (x  y)}
    Γ  x  A
     ------------------
    Γ , y  B  x  A

S′ {x≢y = x≢y} x = S (toWitnessFalse x≢y) x

下面是给自然数二加二赋型的结果:

⊢2+2 :   2+2  `ℕ
⊢2+2 =
  (⊢↓
   (⊢μ
    (⊢ƛ
     (⊢ƛ
      (⊢case (⊢` (S′ Z)) (⊢↑ (⊢` Z) refl)
       (⊢suc
        (⊢↑
         (⊢`
          (S′
           (S′
            (S′ Z)))
          · ⊢↑ (⊢` Z) refl
          · ⊢↑ (⊢` (S′ Z)) refl)
         refl))))))
   · ⊢suc (⊢suc ⊢zero)
   · ⊢suc (⊢suc ⊢zero))

我们可以确认,对相应的项生成类型返回了自然数类型,和上述的推导:

_ : synthesize  2+2  yes  `ℕ , ⊢2+2 
_ = refl

的确,上述的推导是用左边的项求值所得的,再加上一些微小的修改。 所需的修改只是使用智能构造子 S′ 来获取两个变量名(使用字符串)不同的证明(其无法打印或阅读)。

下面是给 Church 数二加二赋型的结果:

⊢2+2ᶜ :   2+2ᶜ  `ℕ
⊢2+2ᶜ =
  ⊢↓
  (⊢ƛ
   (⊢ƛ
    (⊢ƛ
     (⊢ƛ
      (⊢↑
       (⊢`
        (S′
         (S′
          (S′ Z)))
        · ⊢↑ (⊢` (S′ Z)) refl
        ·
        ⊢↑
        (⊢`
         (S′
          (S′ Z))
         · ⊢↑ (⊢` (S′ Z)) refl
         · ⊢↑ (⊢` Z) refl)
        refl)
       refl)))))
  ·
  ⊢ƛ
  (⊢ƛ
   (⊢↑
    (⊢` (S′ Z) ·
     ⊢↑ (⊢` (S′ Z) · ⊢↑ (⊢` Z) refl)
     refl)
    refl))
  ·
  ⊢ƛ
  (⊢ƛ
   (⊢↑
    (⊢` (S′ Z) ·
     ⊢↑ (⊢` (S′ Z) · ⊢↑ (⊢` Z) refl)
     refl)
    refl))
  · ⊢ƛ (⊢suc (⊢↑ (⊢` Z) refl))
  · ⊢zero

我们可以确认,对相应的项生成类型返回了自然数类型,和上述的推导:

_ : synthesize  2+2ᶜ  yes  `ℕ , ⊢2+2ᶜ 
_ = refl

同样,上面的推导使用对左手边的项求值所得,加上一些修改。

测试错误的例子

很重要的是,不仅要检查代码是否按照意图工作,以及代码是否按照意图不工作。 下面是检查不同种类错误的例子:

未约束的变量:

_ : synthesize  ((ƛ "x"  ` "y" )  (`ℕ  `ℕ))  no _
_ = refl

函数应用的参数不是良类型的:

_ : synthesize  (plus · sucᶜ)  no _
_ = refl

函数应用的函数不是良类型的:

_ : synthesize  (plus · sucᶜ · two)  no _
_ = refl

函数应用的函数是自然数类型:

_ : synthesize  ((two  `ℕ) · two)  no _
_ = refl

抽象继承了自然数类型:

_ : synthesize  (twoᶜ  `ℕ)  no _
_ = refl

零继承了函数类型:

_ : synthesize  (`zero  `ℕ  `ℕ)  no _
_ = refl

后继继承了函数类型:

_ : synthesize  (two  `ℕ  `ℕ)  no _
_ = refl

非良类型项的后继:

_ : synthesize  (`suc twoᶜ  `ℕ)  no _
_ = refl

对函数类型的项分情况讨论:

_ : synthesize 
      ((`case (twoᶜ  Ch) [zero⇒ `zero |suc "x"  ` "x"  ]  `ℕ) )  no _
_ = refl

对不良类型的项分情况讨论:

_ : synthesize 
      ((`case (twoᶜ  `ℕ) [zero⇒ `zero |suc "x"  ` "x"  ]  `ℕ) )  no _
_ = refl

变向时继承与生成的项不一致:

_ : synthesize  (((ƛ "x"  ` "x" )  `ℕ  (`ℕ  `ℕ)))  no _
_ = refl

擦除

从装饰过的项拥有正确的类型的证明中,我们可以简单地提取出对应的内在类型的项。 我们使用 DB 来指代 DeBruijn 章节中的代码。 我们可以简单地定义一个擦除(Erasure)从外在的赋型判断,至对应的内在类型的项的函数。

首先,我们给出擦除类型的代码:

∥_∥Tp : Type  DB.Type
 `ℕ ∥Tp             =  DB.`ℕ
 A  B ∥Tp          =   A ∥Tp DB.⇒  B ∥Tp

它简单地把对应的构造子重命名至 DB 模块中。

接下来,我们给出擦除语境的代码:

∥_∥Cx : Context  DB.Context
  ∥Cx              =  DB.∅
 Γ , x  A ∥Cx      =   Γ ∥Cx DB.,  A ∥Tp

它简单地丢弃了变量名。

接下来,我们给出擦除查询判断的代码:

∥_∥∋ :  {Γ x A}  Γ  x  A   Γ ∥Cx DB.∋  A ∥Tp
 Z ∥∋               =  DB.Z
 S x≢ ∋x ∥∋         =  DB.S  ∋x ∥∋

它简单地丢弃了变量名不同的证明。

最后,我们给出擦除赋型判断的代码。 正如由两个共同递归的赋型判断,我们有两个共同递归的擦除函数:

∥_∥⁺ :  {Γ M A}  Γ  M  A   Γ ∥Cx DB.⊢  A ∥Tp
∥_∥⁻ :  {Γ M A}  Γ  M  A   Γ ∥Cx DB.⊢  A ∥Tp

 ⊢` ⊢x ∥⁺           =  DB.`  ⊢x ∥∋
 ⊢L · ⊢M ∥⁺         =   ⊢L ∥⁺ DB.·  ⊢M ∥⁻
 ⊢↓ ⊢M ∥⁺           =   ⊢M ∥⁻

 ⊢ƛ ⊢N ∥⁻           =  DB.ƛ  ⊢N ∥⁻
 ⊢zero ∥⁻           =  DB.`zero
 ⊢suc ⊢M ∥⁻         =  DB.`suc  ⊢M ∥⁻
 ⊢case ⊢L ⊢M ⊢N ∥⁻  =  DB.case  ⊢L ∥⁺  ⊢M ∥⁻  ⊢N ∥⁻
 ⊢μ ⊢M ∥⁻           =  DB.μ  ⊢M ∥⁻
 ⊢↑ ⊢M refl ∥⁻      =   ⊢M ∥⁺

擦除将每个赋型判断中的构造子替换成 DB 中对应的项构造子。 对应变向的构造子被丢弃。

我们证实本章中的赋型判断擦除后,与之前使用内在类型项章节中的那些一致:

_ :  ⊢2+2 ∥⁺  DB.2+2
_ = refl

_ :  ⊢2+2ᶜ ∥⁺  DB.2+2ᶜ
_ = refl

因此,我们证实了双向类型推理把装饰后的 Lambda 章节的 λ 项转换至 DeBruijn 章节中内在类型的项。

练习 inference-multiplication (推荐)

对你在 bidirectional-mul 练习中的乘法项应用双向推理, 并证明推理得到的赋型可以擦除至 DeBruijn 章节中你的定义。

-- 请将代码写在此处。
练习 inference-products (推荐)

使用你在 bidirectional-mul 练习中的赋型规则, 将双向推理扩充至包括积。此外扩充对应的擦除。

-- 请将代码写在此处。
练习 inference-rest (延伸)

使用你从练习 bidirectional-rest 中得到的规则,扩充双向推导规则,来包括 More 章节的剩余构造。 此外扩充对应的擦除。

-- 请将代码写在此处。

Agda 中的双向推理

Agda 本身也使用双向推理。 这解释了为什么构造子可以被重载,而其他定义的名称不可以——此处的重载指的是我们可以给不同类型的构造子使用相同的名称。 构造子是由继承来赋型,而变量由生成来赋型,因此每个变量必须有唯一的类型。

Agda 中多数的顶层声明时函数,其由继承来赋型,也就是为什么 Agda 会对这些定义要求类型声明。 一个由生成赋型的右手边的定义,比如说函数应用,则不需要类型声明。

answer = 6 * 7

Unicode

本章中使用了以下 Unicode:

↓  U+2193:  DOWNWARDS ARROW (\d)
↑  U+2191:  UPWARDS ARROW (\u)
∥  U+2225:  PARALLEL TO (\||)

Untyped: 完全正规化的无类型 λ 演算

module plfa.part2.Untyped where

本章中,我们对于之前的主题加入不同的变化:

  • 之前的章节中讨论了内在类型的演算;我们这次讨论无类型,但是内在作用域的演算。
  • 之前的章节中讨论了传值调用(Call-by-value)的演算;我们这次讨论传名调用(Call-by-name)。
  • 之前的章节中讨论了弱头部范式(Weak Head Normal Form),其规约止步于 λ 抽象;我们这次讨论完全正规化(Full Normalisation),其在 λ 抽象之下仍然继续规约。
  • 之前的章节中讨论了确定性(Deterministic)的规约,每个项中至多有一个可规约项; 我们这次讨论非确定性(Non-deterministic)的规约,每个项中可能有多个可规约项,而每一个都可规约。
  • 之前的章节中讨论了封闭(Closed)的项,其不包含自由变量; 我们这次讨论开放(Open)的项,其可能包含自由变量。
  • 之前的章节中讨论了加入自然数和不动点的 λ 演算; 我们这次讨论只包括变量、抽象和应用的小巧的演算,其他构造均可编码至其中。

一般来说,我们可以将这些特性选择性的混合匹配,而完全正规化要求开放项, 且编码自然数和不动点需要无类型的演算。 本章的目的是展示 λ 演算可能出现的不同形式。

导入

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl)
open import Data.Empty using (; ⊥-elim)
open import Data.Nat using (; zero; suc; _<_; _≤?_; z≤n; s≤s)
open import Relation.Nullary using (¬_)
open import Relation.Nullary.Decidable using (True; toWitness)

无类型即是单一类型

我们的内容将会和 [DeBruijn] 章节中相似,只是每个项会有相同的类型,写作 ,读作『任意』。 这呼应了一条 Dana Scott 提出,Robert Harper 重复的口号:『无类型即是单一类型』。 这样的结果之一就是之前我们需要额外给出的构造(例如自然数和不动点),现在可以在直接在语言本身中定义。

语法

我们首先定义中缀声明:

infix  4  _⊢_
infix  4  _∋_
infixl 5  _,_

infix  6  ƛ_
infix  6  ′_
infixl 7  _·_

类型

我们只有一种类型:

data Type : Set where
   : Type
练习 (Type≃⊤) (习题)

证明 Type 与单元类型 同构。

-- 请将代码写在此处。

语境

和之前一样,语境是类型的列表,最新出现的约束变量的类型出现在最右边:

data Context : Set where
     : Context
  _,_ : Context  Type  Context

我们使用 ΓΔ 来指代语境。

练习 (Context≃ℕ) (习题)

证明 Context 同构。

-- 请将代码写在此处。

变量和查询判断

内在作用域的变量对应了查询判断。规则与之前一样:

data _∋_ : Context  Type  Set where

  Z :  {Γ A}
     ---------
    Γ , A  A

  S_ :  {Γ A B}
     Γ  A
      ---------
     Γ , B  A

我们可以在规则中将所有的 AB 都替换成 ,但不这样做更加清晰。

因为 是唯一的类型,这样的判断并不会给出很多与类型相关的保证。 但它确实确保了所有的变量在作用域内。例如,我们不能在只有两个约束变量的语境中使用 S S Z

项与作用域判断

内类作用域的项对应了赋型判断,但类型只有唯一的 类型。 得到的结果则是我们检查了每个项都是良作用域的——即所有使用的变量都在作用域内——而不是它们是良类型的:

data _⊢_ : Context  Type  Set where

  `_ :  {Γ A}
     Γ  A
      -----
     Γ  A

  ƛ_  :   {Γ}
     Γ ,   
      ---------
     Γ  

  _·_ :  {Γ}
     Γ  
     Γ  
      ------
     Γ  

现在我们有了一个迷你的演算,至包含变量、抽象和应用。 接下来我们展示如果将自然数和不动点编码进这个演算中。

用数表示变量

如之前一样,我们可以将自然数转换为对应的 de Bruijn 因子。 我们不再需要从语境中查询变量的类型,因为每个变量都有一样的类型:

length : Context  
length         =  zero
length (Γ , _)  =  suc (length Γ)

count :  {Γ}  {n : }  (p : n < length Γ)  Γ  
count {Γ , } {zero}    (s≤s z≤n)  =  Z
count {Γ , } {(suc n)} (s≤s p)    =  S (count p)

我们可以接下来引入一种变量的缩略用法:

#_ :  {Γ}
   (n : )
   {n∈Γ : True (suc n ≤? length Γ)}
    --------------------------------
   Γ  
#_ n {n∈Γ}  =  ` count (toWitness n∈Γ)

测试例子

我们唯一的例子是用 Church 数计算二加二:

twoᶜ :  {Γ}  Γ  
twoᶜ = ƛ ƛ (# 1 · (# 1 · # 0))

fourᶜ :  {Γ}  Γ  
fourᶜ = ƛ ƛ (# 1 · (# 1 · (# 1 · (# 1 · # 0))))

plusᶜ :  {Γ}  Γ  
plusᶜ = ƛ ƛ ƛ ƛ (# 3 · # 1 · (# 2 · # 1 · # 0))

2+2ᶜ :   
2+2ᶜ = plusᶜ · twoᶜ · twoᶜ

在之前,我们在遇到 λ 抽象时停止规约,因此我们需要计算 plusᶜ · twoᶜ · twoᶜ · sucᶜ · `zero 来确保我们规约至自然数四。 现在,规约在 λ 之下继续进行,所以我们不需要额外的参数。 为了便利,我们定义用 Church 数来表示二和四的项。

重命名

我们重命名的定义与以前一样。首先我们需要一条扩充引理:

ext :  {Γ Δ}  (∀ {A}  Γ  A  Δ  A)
    -----------------------------------
   (∀ {A B}  Γ , B  A  Δ , B  A)
ext ρ Z      =  Z
ext ρ (S x)  =  S (ρ x)

我们可以在规则中将所有的 AB 都替换成 ,但不这样做更加清晰。

现在定义重命名就很直接了:

rename :  {Γ Δ}
   (∀ {A}  Γ  A  Δ  A)
    ------------------------
   (∀ {A}  Γ  A  Δ  A)
rename ρ (` x)          =  ` (ρ x)
rename ρ (ƛ N)          =  ƛ (rename (ext ρ) N)
rename ρ (L · M)        =  (rename ρ L) · (rename ρ M)

这和之前一样,只是我们项的形式更少了。

同时替换

我们重命名的定义与以前一样。首先我们需要一条扩充引理:

exts :  {Γ Δ}  (∀ {A}  Γ  A  Δ  A)
    ----------------------------------
   (∀ {A B}  Γ , B  A  Δ , B  A)
exts σ Z      =  ` Z
exts σ (S x)  =  rename S_ (σ x)

一样,我们可以把所有的 AB 替换成

现在定义替换就很直接了:

subst :  {Γ Δ}
   (∀ {A}  Γ  A  Δ  A)
    ------------------------
   (∀ {A}  Γ  A  Δ  A)
subst σ (` k)          =  σ k
subst σ (ƛ N)          =  ƛ (subst (exts σ) N)
subst σ (L · M)        =  (subst σ L) · (subst σ M)

同样,这和之前一样,只是我们项的形式更少了。

单个替换

定义替换一个自由变量的特例很简单:

subst-zero :  {Γ B}  (Γ  B)   {A}  (Γ , B  A)  (Γ  A)
subst-zero M Z      =  M
subst-zero M (S x)  =  ` x

_[_] :  {Γ A B}
         Γ , B  A
         Γ  B
          ---------
         Γ  A
_[_] {Γ} {A} {B} N M =  subst {Γ , B} {Γ} (subst-zero M) {A} N

中性项和范式

直到项完全范式化之前,规则可以继续进行。 因此,我们现在在意的是范式(Normal Form),而不是值。 范式的项由与中性项(Neutral Terms)共同递归定义:

data Neutral :  {Γ A}  Γ  A  Set
data Normal  :  {Γ A}  Γ  A  Set

中性项由于我们需要考虑带有自由变量的开放项而产生。 一个项在其为变量时,或者是将中性项应用于范式项时,是一个中性项:

data Neutral where

  `_  :  {Γ A} (x : Γ  A)
      -------------
     Neutral (` x)

  _·_  :  {Γ} {L M : Γ  }
     Neutral L
     Normal M
      ---------------
     Neutral (L · M)

一个项在其为中型项时,或者其为抽象且抽象体是范式时,是一个范式。 我们用 ′_ 来标记中型项。 如果 `_ 一样,它不显眼:

data Normal where

  ′_ :  {Γ A} {M : Γ  A}
     Neutral M
      ---------
     Normal M

  ƛ_  :  {Γ} {N : Γ ,   }
     Normal N
      ------------
     Normal (ƛ N)

我们引入一种缩略用法,来提供变量是中型项的证明:

#′_ :  {Γ} (n : ) {n∈Γ : True (suc n ≤? length Γ)}  Neutral {Γ} (# n)
#′_ n {n∈Γ}  =  ` count (toWitness n∈Γ)

比如说,下面是 Church 数二为范式的证明:

_ : Normal (twoᶜ {})
_ = ƛ ƛ ( #′ 1 · ( #′ 1 · ( #′ 0)))

某一项为范式的证明与其本身基本一致,其中包括的额外的撇来标记中型项,并且其中使用了 #′ 而不是 #

规约步骤

规约规则从传值调用改为传名调用,以实现完全范式化:

  • 规则 ξ₁ 与简单类型的 λ 演算一样,保持不变。
  • 规则 ξ₂ 之中,项 L 是值的要求现在被丢弃了。 所以这条规则现在与 ξ₁ 重合,且规约是非确定的。 现在可选择规约 L 或者 M 中的项。
  • 规则 β 之中,参数是值的要求现在被丢弃了,对应了传名调用的求值。 这引入了更多的非确定性,由于 βξ₂ 在参数中有可规约项时重合。
  • 额外了新规则 ζ,使得 λ 抽象下可以继续规约。

这里是形式化的规则:

infix 2 _—→_

data _—→_ :  {Γ A}  (Γ  A)  (Γ  A)  Set where

  ξ₁ :  {Γ} {L L′ M : Γ  }
     L —→ L′
      ----------------
     L · M —→ L′ · M

  ξ₂ :  {Γ} {L M M′ : Γ  }
     M —→ M′
      ----------------
     L · M —→ L · M′

  β :  {Γ} {N : Γ ,   } {M : Γ  }
      ---------------------------------
     (ƛ N) · M —→ N [ M ]

  ζ :  {Γ} {N N′ : Γ ,   }
     N —→ N′
      -----------
     ƛ N —→ ƛ N′
练习 (variant-1) (习题)

如果我们想要传值调用,但需要项范式化其中的项的话,要怎么样修改规则? 假设 β 在除了两个项都是范式时,不允许规约。

-- 请将代码写在此处。
练习 (variant-2) (习题)

如果我们想要传值调用,但项不在 λ 之下规约的话,要怎么样修改规则? 假设 β 在双项均为值(即 λ 抽象)时允许规约。 2+2ᶜ 在这种情况下会规约成什么?

-- 请将代码写在此处。

自反传递闭包

我们复制粘贴之前的定义:

infix  2 _—↠_
infix  1 begin_
infixr 2 _—→⟨_⟩_
infix  3 _∎

data _—↠_ {Γ A} : (Γ  A)  (Γ  A)  Set where

  _∎ : (M : Γ  A)
      ------
     M —↠ M

  step—→ : (L : Γ  A) {M N : Γ  A}
     M —↠ N
     L —→ M
      ------
     L —↠ N

pattern _—→⟨_⟩_ L L—→M M—↠N = step—→ L M—↠N L—→M

begin_ :  {Γ A} {M N : Γ  A}
   M —↠ N
    ------
   M —↠ N
begin M—↠N = M—↠N

规约序列的例子

这里是二加二得四的展示:

_ : 2+2ᶜ —↠ fourᶜ
_ =
  begin
    plusᶜ · twoᶜ · twoᶜ
  —→⟨ ξ₁ β 
    (ƛ ƛ ƛ twoᶜ · # 1 · (# 2 · # 1 · # 0)) · twoᶜ
  —→⟨ β 
    ƛ ƛ twoᶜ · # 1 · (twoᶜ · # 1 · # 0)
  —→⟨ ζ (ζ (ξ₁ β)) 
    ƛ ƛ ((ƛ # 2 · (# 2 · # 0)) · (twoᶜ · # 1 · # 0))
  —→⟨ ζ (ζ β) 
    ƛ ƛ # 1 · (# 1 · (twoᶜ · # 1 · # 0))
  —→⟨ ζ (ζ (ξ₂ (ξ₂ (ξ₁ β)))) 
    ƛ ƛ # 1 · (# 1 · ((ƛ # 2 · (# 2 · # 0)) · # 0))
  —→⟨ ζ (ζ (ξ₂ (ξ₂ β))) 
   ƛ (ƛ # 1 · (# 1 · (# 1 · (# 1 · # 0))))
  

在两步之后,顶层项是一个抽象,而 ζ 规则支持了剩余的范式化。

可进性

可进性相应地也变更了。之前我们说每个项要么是值,要么可以规约一步;我们现在说每个项要么是范式,要么可以规约一步。

之前,可进性只应用于封闭的、良类型的项。 我们没有考虑诸如应用一个不是函数的项(如 `zero`)或者是带有自由变量的项。 现在我们展示的包含了开放的、良作用域的项。 范式的定义容许了自由变量,且我们也有不是函数的项。

如果一个项可以规约一步,或其为范式,那么它具有可进性:

data Progress {Γ A} (M : Γ  A) : Set where

  step :  {N : Γ  A}
     M —→ N
      ----------
     Progress M

  done :
      Normal M
      ----------
     Progress M

如果一个项是良作用域的,那么它满足可进性:

progress :  {Γ A}  (M : Γ  A)  Progress M
progress (` x)                                 =  done ( ` x)
progress (ƛ N)  with  progress N
... | step N—→N′                               =  step (ζ N—→N′)
... | done NrmN                                =  done (ƛ NrmN)
progress (` x · M) with progress M
... | step M—→M′                               =  step (ξ₂ M—→M′)
... | done NrmM                                =  done ( (` x) · NrmM)
progress ((ƛ N) · M)                           =  step β
progress (L@(_ · _) · M) with progress L
... | step L—→L′                               =  step (ξ₁ L—→L′)
... | done ( NeuL) with progress M
...    | step M—→M′                            =  step (ξ₂ M—→M′)
...    | done NrmM                             =  done ( NeuL · NrmM)

我们对项为良作用域的证明上进行归纳:

  • 如果项是变量,那么它是范式。 (这与之前的证明不同,以往项为变量的情况被闭项的条件所排除了。)
  • 如果项是抽象,那么我们对其抽象体应用可进性。 (这与之前的证明不同,以往抽象本身即是值。):
    • 如果它步进,那么整个项由 ζ 步进。
    • 如果它是范式,那么整个项也是范式。
  • 如果项是应用,那么我们考虑其函数子项:
    • 如果它是变量,我们对参数子项递归应用可进性:
      • 如果它步进,那么整个项由 ξ₂ 步进;
      • 如果它是范式,那么整个项也是范式。
    • 如果它是抽象,那么整个项由 β 步进。
    • 如果它是应用,我们对其函数子项递归应用可进性:
      • 如果它步进,那么整个项由 ξ₁ 步进。
      • 如果它是范式,我们对参数子项递归应用可进性:
        • 如果它步进,那么整个项由 ξ₂ 步进;
        • 如果它是范式,那么整个项也是范式。

可进性最后一条等式中使用了 P@Q 形式的 at 模式,其只在模式 PQ 都匹配时匹配。 @ 是 Agda 不允许出现在变量名中的字符之一,因此不需要在它周围加空格。 在此处,这个模式确保了 L 是一个应用。

求值

与之前一样,可进性直接提供了一个求值器。

汽油由自然数给出:

record Gas : Set where
  constructor gas
  field
    amount : 

当我们的求值器返回项 N,它要么会给出 N 是范式的证明,或者提示汽油耗尽:

data Finished {Γ A} (N : Γ  A) : Set where

   done :
       Normal N
       ----------
      Finished N

   out-of-gas :
       ----------
       Finished N

给定类型 A 的项 L,求值器会对于某 N 返回自 LN 的规约序列,并提示规约是否完成:

data Steps :  {Γ A}  Γ  A  Set where

  steps :  {Γ A} {L N : Γ  A}
     L —↠ N
     Finished N
      ----------
     Steps L

求值器取汽油和项,返回对应的步骤:

eval :  {Γ A}
   Gas
   (L : Γ  A)
    -----------
   Steps L
eval (gas zero)    L                     =  steps (L ) out-of-gas
eval (gas (suc m)) L with progress L
... | done NrmL                          =  steps (L ) (done NrmL)
... | step {M} L—→M with eval (gas m) M
...    | steps M—↠N fin                  =  steps (L —→⟨ L—→M  M—↠N) fin

定义与之前一样,除了我们将空语境 推广至任意语境 Γ

例子

我们重复之前的例子。二加二得四,以 Church 数来表示:

_ : eval (gas 100) 2+2ᶜ 
  steps
   ((ƛ
     (ƛ
      (ƛ
       (ƛ
        (` (S (S (S Z)))) · (` (S Z)) ·
        ((` (S (S Z))) · (` (S Z)) · (` Z))))))
    · (ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z))))
    · (ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z))))
   —→⟨ ξ₁ β 
    (ƛ
     (ƛ
      (ƛ
       (ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z)))) · (` (S Z)) ·
       ((` (S (S Z))) · (` (S Z)) · (` Z)))))
    · (ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z))))
   —→⟨ β 
    ƛ
    (ƛ
     (ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z)))) · (` (S Z)) ·
     ((ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z)))) · (` (S Z)) · (` Z)))
   —→⟨ ζ (ζ (ξ₁ β)) 
    ƛ
    (ƛ
     (ƛ (` (S (S Z))) · ((` (S (S Z))) · (` Z))) ·
     ((ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z)))) · (` (S Z)) · (` Z)))
   —→⟨ ζ (ζ β) 
    ƛ
    (ƛ
     (` (S Z)) ·
     ((` (S Z)) ·
      ((ƛ (ƛ (` (S Z)) · ((` (S Z)) · (` Z)))) · (` (S Z)) · (` Z))))
   —→⟨ ζ (ζ (ξ₂ (ξ₂ (ξ₁ β)))) 
    ƛ
    (ƛ
     (` (S Z)) ·
     ((` (S Z)) ·
      ((ƛ (` (S (S Z))) · ((` (S (S Z))) · (` Z))) · (` Z))))
   —→⟨ ζ (ζ (ξ₂ (ξ₂ β))) 
    ƛ (ƛ (` (S Z)) · ((` (S Z)) · ((` (S Z)) · ((` (S Z)) · (` Z)))))
   )
   (done
    (ƛ
     (ƛ
      (
       (` (S Z)) ·
       ( (` (S Z)) · ( (` (S Z)) · ( (` (S Z)) · ( (` Z)))))))))
_ = refl

自然数和不动点

我们可以使用 Church 数来表示自然数,但是计算某数的前继很复杂且昂贵。 取而代之的,我们使用另一种表示方法,叫做 Scott 数,其核心思想是一个数对应了对其自身的分情况讨论。

回忆 Church 数将某给定函数应用对应的次数。 使用命名的项,我们如下表示前三个 Church 数:

zero  =  ƛ s ⇒ ƛ z ⇒ z
one   =  ƛ s ⇒ ƛ z ⇒ s · z
two   =  ƛ s ⇒ ƛ z ⇒ s · (s · z)

作为对比,我们如下表示前三个 Scott 数:

zero = ƛ s ⇒ ƛ z ⇒ z
one  = ƛ s ⇒ ƛ z ⇒ s · zero
two  = ƛ s ⇒ ƛ z ⇒ s · one

每个数取两个参数,一个对应了后继分支的情况(它要求额外的参数,即当前参数的前继), 一个对应了零分支的情况。 (两种情况可以以任意顺序出现。我们在此将后继分支放在前面,以方便与 Church 数对比。)

下面是以 de Bruijn 因子编码的自然数的 Scott 表示法:

`zero :  {Γ}  (Γ  )
`zero = ƛ ƛ (# 0)

`suc_ :  {Γ}  (Γ  )  (Γ  )
`suc_ M  = (ƛ ƛ ƛ (# 1 · # 2)) · M

case :  {Γ}  (Γ  )  (Γ  )  (Γ ,   )   (Γ  )
case L M N = L · (ƛ N) · M

我们小心地保留之前定义相同的形式。 后继分支期望作用域内有一个额外的变量(由它的类型可以看出),所以它被一个抽象转换成了一个普通的项。

对零使用后继的确规约至 Scott 数表示的一:

_ : eval (gas 100) (`suc_ {} `zero) 
    steps
        ((ƛ (ƛ (ƛ # 1 · # 2))) · (ƛ (ƛ # 0))
    —→⟨ β 
         ƛ (ƛ # 1 · (ƛ (ƛ # 0)))
    )
    (done (ƛ (ƛ ( (` (S Z)) · (ƛ (ƛ ( (` Z))))))))
_ = refl

我们同样可以定义不动点。使用命名的项,我们定义:

μ f = (ƛ x ⇒ f · (x · x)) · (ƛ x ⇒ f · (x · x))

它能实现不动点的原因是:

  μ f
≡
  (ƛ x ⇒ f · (x · x)) · (ƛ x ⇒ f · (x · x))
—→
  f · ((ƛ x ⇒ f · (x · x)) · (ƛ x ⇒ f · (x · x)))
≡
  f · (μ f)

使用 de Bruijn 因子,我们有如下:

μ_ :  {Γ}  (Γ ,   )  (Γ  )
μ N  =  (ƛ ((ƛ (# 1 · (# 0 · # 0))) · (ƛ (# 1 · (# 0 · # 0))))) · (ƛ N)

不动点的参数与之前自然数的后继分支的处理方法相似。

我们可以如之前一样定义二加二:

infix 5 μ_

two :  {Γ}  Γ  
two = `suc `suc `zero

four :  {Γ}  Γ  
four = `suc `suc `suc `suc `zero

plus :  {Γ}  Γ  
plus = μ ƛ ƛ (case (# 1) (# 0) (`suc (# 3 · # 0 · # 1)))

由于 `suc 是定义的项,而不是原语项, plus · two · two 不再规约至 four, 但是它们都规约至相同的范式。

练习 plus-eval (实践)

使用求值器,证实 plus · two · twofour 正规化至相同的项。

-- 请将代码写在此处。
练习 multiplication-untyped (推荐)

使用上文中的编码,翻译你之前章节乘法的定义,使得其使用 Scott 表示法和编码后的不动点运算符。 证实二乘二得四。

-- 请将代码写在此处。
练习 encode-more (延伸)

用上文中类似的方法,编码 More 章节除了原语数字以外的剩余构造,用无类型的 λ 演算。

-- 请将代码写在此处。

多步规约是传递的

在我们对自反传递闭包的形式化,即 —↠ 关系中,没有出现直接的传递性规则。 取而代之的是,这个关系模仿了列表形式,有一种空规约序列的情况,也有一种将一步规约加在规约序列之前的情况。 下面是传递性的证明,其与列表的附加 _++_ 函数的结构相似。

—↠-trans : ∀{Γ}{A}{L M N : Γ  A}
          L —↠ M
          M —↠ N
          L —↠ N
—↠-trans (M ) mn = mn
—↠-trans (L —→⟨ r  lm) mn = L —→⟨ r  (—↠-trans lm mn)

下面的记法使得应用 —↠ 的传递性更加方便。

infixr 2 _—↠⟨_⟩_

_—↠⟨_⟩_ :  {Γ A} (L : Γ  A) {M N : Γ  A}
     L —↠ M
     M —↠ N
      ---------
     L —↠ N
L —↠⟨ L—↠M  M—↠N = —↠-trans L—↠M M—↠N

多步规约是合同性的

回忆 Induction 章节中,一个关系 R 对于一个给定的函数 f 在函数应用后仍然保持关系时,满足合同关系, 即『若 R x y,则 R (f x) (f y)』。 项构造子 ƛ_ and _·_ 是函数,因此合同性的概念也可作用于它们之上。 另外,当一个关系对于所有项的构造子满足合同时,我们说它对于整个语言满足合同性,在此即无类型的 λ 演算。

规则 ξ₁ξ₂ζ 保证了规约关系对于无类型的 λ 演算满足合同性。 多步规约也是一个合同关系,我们在下面三条引理中证明。

appL-cong :  {Γ} {L L' M : Γ  }
          L —↠ L'
           ---------------
          L · M —↠ L' · M
appL-cong {Γ}{L}{L'}{M} (L ) = L · M 
appL-cong {Γ}{L}{L'}{M} (L —→⟨ r  rs) = L · M —→⟨ ξ₁ r  appL-cong rs

appL-cong 的证明对于规约序列 L —↠ L' 进行归纳。 * 若 L —↠ LL ∎ 成立。那我们可从 L · M ∎L · M —↠ L · M 。 * 若 L —↠ L''L —→⟨ r ⟩ rs 成立,那么 L —→ L'r 成立,且 L' —↠ L''rs 成立。 我们可从 ξ₁ rL · M —→ L' · M,且对 rs 应用归纳假设得到 L' · M —↠ L'' · M。 我们可以将两者用 _—→⟨_⟩_ 组合来获得 L · M —↠ L'' · M

appR-congabs-cong 的证明与 appL-cong 的证明使用一样的方法。

appR-cong :  {Γ} {L M M' : Γ  }
          M —↠ M'
           ---------------
          L · M —↠ L · M'
appR-cong {Γ}{L}{M}{M'} (M ) = L · M 
appR-cong {Γ}{L}{M}{M'} (M —→⟨ r  rs) = L · M —→⟨ ξ₂ r  appR-cong rs
abs-cong :  {Γ} {N N' : Γ ,   }
          N —↠ N'
           ----------
          ƛ N —↠ ƛ N'
abs-cong (M ) = ƛ M 
abs-cong (L —→⟨ r  rs) = ƛ L —→⟨ ζ r  abs-cong rs

Unicode

本章使用了下列 Unicode:

★  U+2605  BLACK STAR (\st)

\st 命令允许选取不同种类的星,我们使用的是第七种。

Confluence: 无类型 λ-演算的合流性

module plfa.part2.Confluence where

简介

在这一章我们将证明 β-规约是合流的(Confluent) (又被称作 Church-Rosser 性质): 如果有从任一项 L 至两个不同项 M₁M₂ 的规约序列, 那么一定存在从这两个项至某个相同项 N 的规约序列。 如图:

    L
   / \
  /   \
 /     \
M₁      M₂
 \     /
  \   /
   \ /
    N

其中向下的线是 -↠ 的实例。

合流性(Confluence)也在许多 λ-演算外的重写系统中被研究, 并且如何在满足菱形性质(Diamond Property), 一种合流性的单步版本的重写系统中证明合流性是广为人知的。 令 为一个关系。 具有菱形性质, 如果对于任何 L ⇛ M₁L ⇛ M₂ 都存在 N , 使得 M₁ ⇛ NM₂ ⇛ N。 这只是上图的另一个例子,此处向下的线代表 。 如果我们将 的自反传递闭包写作 ⇛*, 那么可以立即从菱形性质推出 ⇛* 的合流性。

不幸的是 λ-演算的规约并不满足菱形性质。这是一个反例:

(λ x. x x)((λ x. x) a) —→ (λ x. x x) a
(λ x. x x)((λ x. x) a) —→ ((λ x. x) a) ((λ x. x) a)

两个项都可以规约至 a a,但第二项需要两步来完成,而不是一步。

为了回避这个问题,我们将定义一个辅助规约关系, 称为平行规约(Parallel Reduction) , 它可以同时执行许多规约并因此满足菱形性质。 更进一步地,我们将证明在两个项间存在平行规约序列当且仅当这两个项间存在 β-规约序列。 因此,我们可以将 β-规约序列合流性的证明约化为平行规约合流性的证明。

导入

open import Relation.Binary.PropositionalEquality using (_≡_; refl)
open import Function using (_∘_)
open import Data.Product using (_×_; Σ; Σ-syntax; ; ∃-syntax; proj₁; proj₂)
  renaming (_,_ to ⟨_,_⟩)
open import plfa.part2.Substitution using (Rename; Subst)
open import plfa.part2.Untyped
  using (_—→_; β; ξ₁; ξ₂; ζ; _—↠_; begin_; _—→⟨_⟩_; _—↠⟨_⟩_; _∎;
  abs-cong; appL-cong; appR-cong; —↠-trans;
  _⊢_; _∋_; `_; #_; _,_; ; ƛ_; _·_; _[_];
  rename; ext; exts; Z; S_; subst; subst-zero)

平行规约

平行规约关系被定义如下。

infix 2 _⇛_

data _⇛_ :  {Γ A}  (Γ  A)  (Γ  A)  Set where

  pvar : ∀{Γ A}{x : Γ  A}
      ---------
     (` x)  (` x)

  pabs : ∀{Γ}{N N′ : Γ ,   }
     N  N′
      ----------
     ƛ N  ƛ N′

  papp : ∀{Γ}{L L′ M M′ : Γ  }
     L  L′
     M  M′
      -----------------
     L · M  L′ · M′

  pbeta : ∀{Γ}{N N′  : Γ ,   }{M M′ : Γ  }
     N  N′
     M  M′
      -----------------------
     (ƛ N) · M    N′ [ M′ ]

前三种规则是同时规约每个部分的合同性。 最后一个规则平行地规约一个 λ-项和另一个项,接着是一步 β-规约。

我们注意到 pabspapppbeta 规则同时对它们的所有子表达式执行归约。 此外,pabs 规则类似于 ζ 规则,pbeta 类似于 β 规则。

平行规约是自反的。

par-refl : ∀{Γ A}{M : Γ  A}  M  M
par-refl {Γ} {A} {` x} = pvar
par-refl {Γ} {} {ƛ N} = pabs par-refl
par-refl {Γ} {} {L · M} = papp par-refl par-refl

我们定义平行规约序列如下。

infix  2 _⇛*_
infixr 2 _⇛⟨_⟩_
infix  3 _∎

data _⇛*_ :  {Γ A}  (Γ  A)  (Γ  A)  Set where

  _∎ :  {Γ A} (M : Γ  A)
      --------
     M ⇛* M

  _⇛⟨_⟩_ :  {Γ A} (L : Γ  A) {M N : Γ  A}
     L  M
     M ⇛* N
      ---------
     L ⇛* N
练习 par-diamond-eg(实践)

回顾上文中菱形性质的反例,证明在这种情况下平行规约具有菱形性质。

-- 请将代码写在此处。

平行规约与规约间等价性

此处我们证明对于任何 MNM ⇛* N 当且仅当 M —↠ N。 必要性的证明非常容易,我们开始于说明若 M —→ N,则 M ⇛ N。 该证明通过对规约 M —→ N 进行归纳。

beta-par : ∀{Γ A}{M N : Γ  A}
   M —→ N
    ------
   M  N
beta-par {Γ} {} {L · M} (ξ₁ r) = papp (beta-par {M = L} r) par-refl
beta-par {Γ} {} {L · M} (ξ₂ r) = papp par-refl (beta-par {M = M} r)
beta-par {Γ} {} {(ƛ N) · M} β = pbeta par-refl par-refl
beta-par {Γ} {} {ƛ N} (ζ r) = pabs (beta-par r)

证明了该引理后我们便可完成必要性的证明, 即 M —↠ N 蕴含 M ⇛* N。该证明是对 M —↠ N 规约序列的简单归纳。

betas-pars : ∀{Γ A} {M N : Γ  A}
   M —↠ N
    ------
   M ⇛* N
betas-pars {Γ} {A} {M₁} {.M₁} (M₁ ) = M₁ 
betas-pars {Γ} {A} {.L} {N} (L —→⟨ b  bs) =
   L ⇛⟨ beta-par b  betas-pars bs

现在考虑命题的充分性,即 M ⇛* N 蕴含 M —↠ N。 充分性的证明有一点不同,因为它不是 M ⇛ N 蕴含 M —→ N 的情形。 毕竟 M ⇛ N 执行了许多规约, 所以我们应当证明 M ⇛ N 蕴含 M —↠ N

par-betas : ∀{Γ A}{M N : Γ  A}
   M  N
    ------
   M —↠ N
par-betas {Γ} {A} {.(` _)} (pvar{x = x}) = (` x) 
par-betas {Γ} {} {ƛ N} (pabs p) = abs-cong (par-betas p)
par-betas {Γ} {} {L · M} (papp {L = L}{L′}{M}{M′} p₁ p₂) =
    begin
    L · M   —↠⟨ appL-cong{M = M} (par-betas p₁) 
    L′ · M  —↠⟨ appR-cong (par-betas p₂) 
    L′ · M′
    
par-betas {Γ} {} {(ƛ N) · M} (pbeta{N′ = N′}{M′ = M′} p₁ p₂) =
    begin
    (ƛ N) · M                    —↠⟨ appL-cong{M = M} (abs-cong (par-betas p₁)) 
    (ƛ N′) · M                   —↠⟨ appR-cong{L = ƛ N′} (par-betas p₂)  
    (ƛ N′) · M′                  —→⟨ β 
     N′ [ M′ ]
    

该证明通过对 M ⇛ N 进行归纳。

  • 假定 x ⇛ x。我们立刻有 x —↠ x

  • 假定 ƛ N ⇛ ƛ N′ 因为 N ⇛ N′。根据归纳假设我们有 N —↠ N′。 我们得出 ƛ N —↠ ƛ N′ 因为 —↠ 具有合同性。

  • 假定 L · M ⇛ L′ · M′ 因为 L ⇛ L′M ⇛ M′。 根据归纳假设,我们有 L —↠ L′M —↠ M′。 所以有 L · M —↠ L′ · M 以及 L′ · M —↠ L′ · M′ 因为 —↠ 具有合同性。

  • 假定 (ƛ N) · M ⇛ N′ [ M′ ] 因为 N ⇛ N′M ⇛ M′。 根据类似的原因,我们有 (ƛ N) · M —↠ (ƛ N′) · M′, 接着应用 β-规约我们得到 (ƛ N′) · M′ —→ N′ [ M′ ]

证明了该引理后我们便可通过对 M ⇛* N 的一步简单归纳完成 M ⇛* N 蕴含 M —↠ N 的证明。

pars-betas : ∀{Γ A} {M N : Γ  A}
   M ⇛* N
    ------
   M —↠ N
pars-betas (M₁ ) = M₁ 
pars-betas (L ⇛⟨ p  ps) = —↠-trans (par-betas p) (pars-betas ps)

平行规约的替换引理

我们的下一个目标是对平行规约证明菱形性质。 为了完成该证明,我们还需证明替换遵从平行规约。 也就是说,如果有 N ⇛ N′M ⇛ M′,那么 N [ M ] ⇛ N′ [ M′ ]。 我们不能直接通过归纳证明它,所以我们将其推广为: 如果 N ⇛ N′ 并且替换 σ 逐点(Pointwise)平行规约至 τ, 则 subst σ N ⇛ subst τ N′。 我们如下定义逐点平行规约。

par-subst : ∀{Γ Δ}  Subst Γ Δ  Subst Γ Δ  Set
par-subst {Γ}{Δ} σ σ′ = ∀{A}{x : Γ  A}  σ x  σ′ x

因为替换依赖于扩展函数 exts,而其又依赖于 rename, 我们开始于被称为 par-rename 的替换引理的一种版本,该引理专门用于重命名。 par-rename 依赖于重命名和替换可以相互交换的事实, 这是一个我们在 Substitution 章节引入并在此处重申的引理。

rename-subst-commute : ∀{Γ Δ}{N : Γ ,   }{M : Γ  }{ρ : Rename Γ Δ }
     (rename (ext ρ) N) [ rename ρ M ]  rename ρ (N [ M ])
rename-subst-commute {N = N} = plfa.part2.Substitution.rename-subst-commute {N = N}

现在证明 par-rename 引理。

par-rename : ∀{Γ Δ A} {ρ : Rename Γ Δ} {M M′ : Γ  A}
   M  M′
    ------------------------
   rename ρ M  rename ρ M′
par-rename pvar = pvar
par-rename (pabs p) = pabs (par-rename p)
par-rename (papp p₁ p₂) = papp (par-rename p₁) (par-rename p₂)
par-rename {Γ}{Δ}{A}{ρ} (pbeta{Γ}{N}{N′}{M}{M′} p₁ p₂)
    with pbeta (par-rename{ρ = ext ρ} p₁) (par-rename{ρ = ρ} p₂)
... | G rewrite rename-subst-commute{Γ}{Δ}{N′}{M′}{ρ} = G

证明通过对 M ⇛ M′ 进行归纳来完成。前三种情况很简单, 所以我们只考虑最后一种,即 pbeta

  • 假定 (ƛ N) · M ⇛ N′ [ M′ ] 因为 N ⇛ N′M ⇛ M′。 根据归纳假设,我们有 rename (ext ρ) N ⇛ rename (ext ρ) N′rename ρ M ⇛ rename ρ M′。 所以根据 pbeta 我们有 (ƛ rename (ext ρ) N) · (rename ρ M) ⇛ (rename (ext ρ) N) [ rename ρ M ]。 然而,为了得出结论我们需要平行规约至 rename ρ (N [ M ])。 值得庆幸的是,重命名和规约可以相互交换。

有了 par-rename 引理,很容易证明扩展替换保留了逐点平行归约关系。

par-subst-exts : ∀{Γ Δ} {σ τ : Subst Γ Δ}
   par-subst σ τ
    ------------------------------------------
   ∀{B}  par-subst (exts σ {B = B}) (exts τ)
par-subst-exts s {x = Z} = pvar
par-subst-exts s {x = S x} = par-rename s

为了证明替换遵从平行规约关系,我们需要证明的下一个引理如下文所示, 它声称同时规约可以与单步规约相交换。 我们从 Substitution 章节导入这个引理, 并重申如下。

subst-commute : ∀{Γ Δ}{N : Γ ,   }{M : Γ  }{σ : Subst Γ Δ }
   subst (exts σ) N [ subst σ M ]  subst σ (N [ M ])
subst-commute {N = N} = plfa.part2.Substitution.subst-commute {N = N}

我们准备好去证明替换遵从平行规约。

subst-par : ∀{Γ Δ A} {σ τ : Subst Γ Δ} {M M′ : Γ  A}
   par-subst σ τ    M  M′
    --------------------------
   subst σ M  subst τ M′
subst-par {Γ} {Δ} {A} {σ} {τ} {` x} s pvar = s
subst-par {Γ} {Δ} {A} {σ} {τ} {ƛ N} s (pabs p) =
  pabs (subst-par {σ = exts σ} {τ = exts τ}
         {A}{x}  par-subst-exts s {x = x}) p)
subst-par {Γ} {Δ} {} {σ} {τ} {L · M} s (papp p₁ p₂) =
  papp (subst-par s p₁) (subst-par s p₂)
subst-par {Γ} {Δ} {} {σ} {τ} {(ƛ N) · M} s (pbeta{N′ = N′}{M′ = M′} p₁ p₂)
    with pbeta (subst-par{σ = exts σ}{τ = exts τ}{M = N}
                        (λ{A}{x}  par-subst-exts s {x = x}) p₁)
               (subst-par {σ = σ} s p₂)
... | G rewrite subst-commute{N = N′}{M = M′}{σ = τ} = G

我们通过对 M ⇛ M′ 归纳来证明。

  • 假定 x ⇛ x。我们使用前提 par-subst σ τ 来得出 σ x ⇛ τ x

  • 假定 ƛ N ⇛ ƛ N′ 因为 N ⇛ N′。 为了使用归纳假设,我们需要 par-subst (exts σ) (exts τ), 它通过 par-subst-exts 得出。 所以我们有 subst (exts σ) N ⇛ subst (exts τ) N′ 并且通过规则 pabs 来完成证明。

  • 假定 L · M ⇛ L′ · M′ 因为 L ⇛ L′M ⇛ M′。 根据归纳假设我们有 subst σ L ⇛ subst τ L′subst σ M ⇛ subst τ M′, 所以我们通过规则 papp 来完成证明。

  • 假定 (ƛ N) · M ⇛ N′ [ M′ ] 因为 N ⇛ N′M ⇛ M′。 同样我们根据 par-subst-exts 来得到 par-subst (exts σ) (exts τ)。 所以根据归纳假设,我们有 subst (exts σ) N ⇛ subst (exts τ) N′subst σ M ⇛ subst τ M′。 接着根据 pbeta 规则,我们平行规约至 subst (exts τ) N′ [ subst τ M′ ]。 替换在以下意义中与自身交换: 对于任意 σ、 N 和 M, 我们有

      (subst (exts σ) N) [ subst σ M ] ≡ subst σ (N [ M ])

    所以我们平行规约得到 subst τ (N′ [ M′ ])

显然,若 M ⇛ M′,则 subst-zero M 逐点平行规约至 subst-zero M′

par-subst-zero : ∀{Γ}{A}{M M′ : Γ  A}
        M  M′
        par-subst (subst-zero M) (subst-zero M′)
par-subst-zero {M} {M′} p {A} {Z} = p
par-subst-zero {M} {M′} p {A} {S x} = pvar

我们以所期望的推论来结束本节,即替换遵从平行规约。

sub-par : ∀{Γ A B} {N N′ : Γ , A  B} {M M′ : Γ  A}
   N  N′
   M  M′
    --------------------------
   N [ M ]  N′ [ M′ ]
sub-par pn pm = subst-par (par-subst-zero pm) pn

平行规约满足菱形性质

合流性证明的核心是石头制成的,更确切地说,是钻石! 【译注:在英文中 diamond 一词既指钻石,又指菱形。】 我们将证明平行规约满足菱形性质,即若有 M ⇛ NM ⇛ N′, 那么对某个 LN ⇛ LN′ ⇛ L。 典型的证明通过对 M ⇛ NM ⇛ N′ 归纳来完成, 因此每一个可能的对都会在执行足够多次平行 β-规约后产生一个证明 L

然而,一个更简单的方法是对 M 执行尽可能多次平行 β-规约, 称其为 M ⁺,然后证明 N 也可以平行规约至 M ⁺。 这就是 Takahashi 的 complete development 的想法。 所需的性质可以表示为:

    M
   /|
  / |
 /  |
N   2
 \  |
  \ |
   \|
    M⁺

其中向下的线是 的实例。因此我们称其为三角性质(Triangle Property)

_⁺ :  {Γ A}
   Γ  A  Γ  A
(` x)        =  ` x
(ƛ M)        = ƛ (M )
((ƛ N) · M)  = N  [ M  ]
(L · M)      = L  · (M )

par-triangle :  {Γ A} {M N : Γ  A}
   M  N
    -------
   N  M 
par-triangle pvar          = pvar
par-triangle (pabs p)      = pabs (par-triangle p)
par-triangle (pbeta p1 p2) = sub-par (par-triangle p1) (par-triangle p2)
par-triangle (papp {L = ƛ _ } (pabs p1) p2) =
  pbeta (par-triangle p1) (par-triangle p2)
par-triangle (papp {L = ` _}   p1 p2) = papp (par-triangle p1) (par-triangle p2)
par-triangle (papp {L = _ · _} p1 p2) = papp (par-triangle p1) (par-triangle p2)

三角性质的证明通过对 M ⇛ N 归纳来完成。

  • 假定 x ⇛ x。显然 x ⁺ = x,所以 x ⇛ x ⁺

  • 假定 ƛ M ⇛ ƛ N。根据归纳假设我们有 N ⇛ M ⁺, 并且根据定义我们有 (λ M) ⁺ = λ (M ⁺),所以我们得出 λ N ⇛ λ(M ⁺)

  • 假定 (λ N) · M ⇛ N′ [ M′ ]。根据归纳假设我们有 N′ ⇛ N ⁺M′ ⇛ M ⁺。 因为替换遵从平行规约,于是得到 N′ [ M′ ] ⇛ N ⁺ [ M ⁺ ], 而右侧即为 ((λ N) · M) ⁺,因此有 N′ [ M′ ] ⇛ ((λ N) · M) ⁺

  • 假定 (λ L) · M ⇛ (λ L′) · M′。 根据归纳假设我们有 L′ ⇛ L ⁺M′ ⇛ M ⁺; 根据定义有 ((λ L) · M) ⁺ = L ⁺ [ M ⁺ ]。 接着得出 (λ L′) · M′ ⇛ L ⁺ [ M ⁺ ]

  • 假定 x · M ⇛ x · M′。 根据归纳假设我们有 M′ ⇛ M ⁺x ⇛ x ⁺,因此 x · M′ ⇛ x · M ⁺。 剩余的情况可以以相同方式证明,所以我们忽略它。 (由于 Agda 目前没有办法在检查右侧之前为我们扩展 _⁺ 定义中的 catch-all 模式,我们必须明确地写下剩余的情况。)

菱形性质接着通过将菱形折半成两个三角形得出。

    M
   /|\
  / | \
 /  |  \
N   2   N′
 \  |  /
  \ | /
   \|/
    M⁺

也就是说,菱形性质通过在两侧应用具有相同合流项 M ⁺的三角性质而证明。

par-diamond : ∀{Γ A} {M N N′ : Γ  A}
   M  N
   M  N′
    ---------------------------------
   Σ[ L  Γ  A ] (N  L) × (N′  L)
par-diamond {M = M} p1 p2 =  M  ,  par-triangle p1 , par-triangle p2  

不过,在存在三角性质的情况下,该步骤是可选的。

练习(实践)
  • 通过对 M ⇛ NM ⇛ N′ 归纳直接证明菱形性质 par-diamond

  • 作图表示 par-diamond 的直接证明中的六种情况。 图应当包含节点和有向边,其中节点用项标记,边代表平行规约。

平行规约合流性的证明

像在开始承诺的那样,平行规约合流性的证明现在十分简单, 因为我们知道它满足三角性质。 我们只需证明带状引理(Strip Lemma),它声称若有 M ⇛ NM ⇛* N′, 则对于某个 LN ⇛* LN′ ⇛ L。 下图解释了带状引理:

    M
   / \
  1   *
 /     \
N       N′
 \     /
  *   1
   \ /
    L

此处向下的线是 ⇛* 的实例,取决于它们如何被标记。

带状引理的证明是对 M ⇛* N′ 的简单归纳,并在归纳步骤使用三角性质。

strip : ∀{Γ A} {M N N′ : Γ  A}
   M  N
   M ⇛* N′
    ------------------------------------
   Σ[ L  Γ  A ] (N ⇛* L)  ×  (N′  L)
strip{Γ}{A}{M}{N}{N′} mn (M ) =  N ,  N  , mn  
strip{Γ}{A}{M}{N}{N′} mn (M ⇛⟨ mm'  m'n')
  with strip (par-triangle mm') m'n'
... |  L ,  ll' , n'l'   =  L ,  N ⇛⟨ par-triangle mn  ll' , n'l'  

平行规约合流性的证明现在通过对序列 M ⇛* N 归纳来完成,并在归纳步骤使用上述引理。

par-confluence : ∀{Γ A} {L M₁ M₂ : Γ  A}
   L ⇛* M₁
   L ⇛* M₂
    ------------------------------------
   Σ[ N  Γ  A ] (M₁ ⇛* N) × (M₂ ⇛* N)
par-confluence {Γ}{A}{L}{.L}{N} (L ) L⇛*N =  N ,  L⇛*N , N   
par-confluence {Γ}{A}{L}{M₁′}{M₂} (L ⇛⟨ L⇛M₁  M₁⇛*M₁′) L⇛*M₂
    with strip L⇛M₁ L⇛*M₂
... |  N ,  M₁⇛*N , M₂⇛N  
      with par-confluence M₁⇛*M₁′ M₁⇛*N
...   |  N′ ,  M₁′⇛*N′ , N⇛*N′   =
         N′ ,  M₁′⇛*N′ , (M₂ ⇛⟨ M₂⇛N  N⇛*N′)  

归纳步骤可以如下图解释:

        L
       / \
      1   *
     /     \
    M₁ (a)  M₂
   / \     /
  *   *   1
 /     \ /
M₁′(b)  N
 \     /
  *   *
   \ /
    N′

此处向下的线是 ⇛* 的实例,取决于它们如何被标记。 此处 (a) 根据 strip 而成立,(b) 根据归纳假设而成立。

规约合流性的证明

规约的合流性是平行规约合流性的一个推论。 从 L —↠ M₁L —↠ M₂ 根据 betas-pars 我们有 L ⇛* M₁L ⇛* M₂。 接着根据合流性我们得到对于某个 LM₁ ⇛* NM₂ ⇛* N。 因此我们根据 pars-betas 得出 M₁ —↠ NM₂ —↠ N

confluence : ∀{Γ A} {L M₁ M₂ : Γ  A}
   L —↠ M₁
   L —↠ M₂
    -----------------------------------
   Σ[ N  Γ  A ] (M₁ —↠ N) × (M₂ —↠ N)
confluence L↠M₁ L↠M₂
    with par-confluence (betas-pars L↠M₁) (betas-pars L↠M₂)
... |  N ,  M₁⇛N , M₂⇛N   =
       N ,  pars-betas M₁⇛N , pars-betas M₂⇛N  

注记

总而言之,这种机械化的合流性证明基于几个来源。 subst-par 引理是 Schäfer, Tebbi, and Smolka (2015) 的「强替换性(Strong Substitutivity)」 引理。 par-trianglestrippar-confluence 的证明基于 Takahashi (1995) 完全发展的概念,以及 Pfenning (1992) 关于 Church-Rosser 定理的技术报告。 此外,我们询问了 Nipkow 和 Berghofer 在 Isabelle 中的机械化, 它基于 Nipkow (1996) 的早期论文。

Unicode

本章中使用了以下 Unicode:

⇛  U+21DB  RIGHTWARDS TRIPLE ARROW (\r== or \Rrightarrow)
⁺  U+207A  SUPERSCRIPT PLUS SIGN   (\^+)

BigStep: 无类型 λ-演算的大步语义

module plfa.part2.BigStep where

简介

传名调用求值策略(Call-by-name Evaluation Strategy)是在 λ-演算中计算程序值的一种确定性方法。 也就是说,传名调用能够求出值当且仅当 β-规约能将程序规约为一个 λ-抽象。 在这一章节,我们将定义传名调用求值并且证明这个等价命题的必要性。 充分性的部分较为复杂,通常通过 Curry-Feys 标准化证明。 根据 Plotkin 的工作,我们给出这个证明的概要, 但是由于这是指称语义的一个简单性质, 我们将在为 λ-演算发展出指称语义后在 Agda 中完整地证明它。

我们将传名调用策略表示为一个输入表达式与输出值间的关系。 因为这样的关系将输入表达式 M 和最终结果 V 直接相联系, 它通常被叫做大步语义(Big-stepsemantics),写做 M ⇓ V。 而小步规约关系则被写做 M —→ M′,它仅通过一步子计算来将 M 规约为另一个表达式 M′

导入

open import Relation.Binary.PropositionalEquality
  using (_≡_; refl; trans; sym; cong-app)
open import Data.Product using (_×_; Σ; Σ-syntax; ; ∃-syntax; proj₁; proj₂)
  renaming (_,_ to ⟨_,_⟩)
open import Function using (_∘_)
open import plfa.part2.Untyped
  using (Context; _⊢_; _∋_; ; ; _,_; Z; S_; `_; #_; ƛ_; _·_;
  subst; subst-zero; exts; rename; β; ξ₁; ξ₂; ζ; _—→_; _—↠_; _—→⟨_⟩_; _∎;
  —↠-trans; appL-cong)
open import plfa.part2.Substitution using (Subst; ids)

环境

为了处理变量和函数应用,我们要么像在 —→ 中一样使用替换,要么使用一个环境(Environment)。 传名调用中的环境是一个从变量到闭包(即项与其对应的环境)的映射。 我们之所以使用环境取代替换是因为传名调用的核心更接近于语言的实现。 在后续章节中介绍的指称语义也会用到环境,而且对 adequacy 的证明也会变得更加容易。

我们如下定义环境和闭包。

ClosEnv : Context  Set

data Clos : Set where
  clos : ∀{Γ}  (M : Γ  )  ClosEnv Γ  Clos

ClosEnv Γ =  (x : Γ  )  Clos

通常,我们有空环境,也可以扩展一个环境。

∅' : ClosEnv 
∅' ()

_,'_ :  {Γ}  ClosEnv Γ  Clos  ClosEnv (Γ , )
(γ ,' c) Z = c
(γ ,' c) (S x) = γ x

大步求值

大步语义被表现为一个三元关系,写作 γ ⊢ M ⇓ V, 其中 γ 是环境,M是输入项,V 是结果值。 值(Value) 是一个项是 λ-抽象的闭包。

data _⊢_⇓_ : ∀{Γ}  ClosEnv Γ  (Γ  )  Clos  Set where

  ⇓-var : ∀{Γ}{γ : ClosEnv Γ}{x : Γ  }{Δ}{δ : ClosEnv Δ}{M : Δ  }{V}
     γ x  clos M δ
     δ  M  V
      -----------
     γ  ` x  V

  ⇓-lam : ∀{Γ}{γ : ClosEnv Γ}{M : Γ ,   }
     γ  ƛ M  clos (ƛ M) γ

  ⇓-app : ∀{Γ}{γ : ClosEnv Γ}{L M : Γ  }{Δ}{δ : ClosEnv Δ}{N : Δ ,   }{V}
     γ  L  clos (ƛ N) δ      (δ ,' clos M γ)  N  V
      ---------------------------------------------------
     γ  L · M  V
  • ⇓-var 规则通过对环境中找到的相关闭包求值,从而完成对变量的求值。

  • ⇓-lam 规则通过包装 λ-抽象与其环境,将其转变为一个闭包。

  • ⇓-app 规则分两步处理函数应用。首先对操作位的项 L 求值,如果产生了一个包含 λ-抽象 ƛ N 的闭包, 就在扩展了参数 M 的环境中对 N 求值。注意到 M 并未在 ⇓-app 规则中被求值, 因为进行的是传名调用而不是传值调用。

练习 big-step-eg(实践)

证明 (ƛ ƛ # 1) · ((ƛ # 0 · # 0) · (ƛ # 0 · # 0)) 能在大步传名调用求值下终止。

-- 请将代码写在此处。

大步语义是确定的

如果大步关系将一个项 M 求值为 VV′,则 VV′ 必然相同。 也就是说,传名调用关系是一个部分函数。该证明由两个大步语义的推论归纳得出。

⇓-determ : ∀{Γ}{γ : ClosEnv Γ}{M : Γ  }{V V' : Clos}
   γ  M  V  γ  M  V'
   V  V'
⇓-determ (⇓-var eq1 mc) (⇓-var eq2 mc')
    with trans (sym eq1) eq2
... | refl = ⇓-determ mc mc'
⇓-determ ⇓-lam ⇓-lam = refl
⇓-determ (⇓-app mc mc₁) (⇓-app mc' mc'')
    with ⇓-determ mc mc'
... | refl = ⇓-determ mc₁ mc''

大步求值蕴含 β-规约至 λ-抽象

如果大步求值能够求出值,那么输入项能被 β-规约规约为一个 λ-抽象:

  ∅' ⊢ M ⇓ clos (ƛ N′) δ
  -----------------------------
→ Σ[ N ∈ ∅ , ★ ⊢ ★ ] (M —↠ ƛ N)

该证明通过对大步推导归纳来完成。通常,我们需要推广命题以完成归纳。 在 ⇓-app(函数应用)的情况下,参数被添加到环境中,导致环境变得非空。 相应的 β-规约将参数替换进 λ-抽象的主体中。 所以我们将引理推广为允许任意环境 γ 并且添加一个前提将环境 γ 与等价的替代 σ 相关联。

⇓-app 的情况也要求我们加强结论。 对于 ⇓-app,有 γ ⊢ L ⇓ clos (λ N) δ 以及归纳假设给我们 L —↠ ƛ N′, 但我们需要知道 NN′ 是等价的。 特别地,N' ≡ subst τ N,其中 τ 是等价于 δ 的替换。 因此我们扩展命题的结论,以说明这两个结果是等价的。

我们通过定义以下两个相互递归的谓词 V ≈ Mγ ≈ₑ σ 来得到两个精确等价的概念

_≈_ : Clos  (  )  Set
_≈ₑ_ : ∀{Γ}  ClosEnv Γ  Subst Γ   Set

(clos {Γ} M γ)  N = Σ[ σ  Subst Γ  ] γ ≈ₑ σ × (N  subst σ M)

γ ≈ₑ σ =  x  (γ x)  (σ x)

现在我们可以给出主要的引理。

如果 γ ⊢ M ⇓ V  且  γ ≈ₑ σ,
那么有  subst σ M —↠ N  以及  V ≈ N  对于某个 N。

在开始证明之前,我们需要建立一些有关等价的环境和替换的引理。

空环境与我们从 Substitution 章中导入的恒等替换等价。

≈ₑ-id : ∅' ≈ₑ ids
≈ₑ-id ()

显然,对项应用恒等替换会得到相同的项。

sub-id : ∀{Γ} {A} {M : Γ  A}  subst ids M  M
sub-id = plfa.part2.Substitution.sub-id

我们定义一个辅助函数来扩展替换。

ext-subst : ∀{Γ Δ}  Subst Γ Δ  Δ    Subst (Γ , ) Δ
ext-subst{Γ}{Δ} σ N {A} = subst (subst-zero N)  exts σ

下一个需要证明的引理声称如果从等价的环境和替换 γ ≈ₑ σ 开始, 将它们用等价的闭包和项 c ≈ N 扩展, 将得到等价的环境和替换 (γ ,' V) ≈ₑ (ext-subst σ N), 即对于任何变量 x(γ ,' V) x ≈ₑ (ext-subst σ N) x。 证明将通过归纳 x 完成,并且我们需要如下引理。 该引理声称将exts σsubst-zero 的组合应用至 S x 等同于 σ x。 这是 Substitution 章节中一个定理的推论。

subst-zero-exts : ∀{Γ Δ}{σ : Subst Γ Δ}{B}{M : Δ  B}{x : Γ  }
   (subst (subst-zero M)  exts σ) (S x)  σ x
subst-zero-exts {Γ}{Δ}{σ}{B}{M}{x} =
   cong-app (plfa.part2.Substitution.subst-zero-exts-cons{σ = σ}) (S x)

所以 ≈ₑ-ext 的证明如下。

≈ₑ-ext :  {Γ} {γ : ClosEnv Γ} {σ : Subst Γ } {V} {N :   }
   γ ≈ₑ σ    V  N
    --------------------------
   (γ ,' V) ≈ₑ (ext-subst σ N)
≈ₑ-ext {Γ} {γ} {σ} {V} {N} γ≈ₑσ V≈N Z = V≈N
≈ₑ-ext {Γ} {γ} {σ} {V} {N} γ≈ₑσ V≈N (S x)
  rewrite subst-zero-exts {σ = σ}{M = N}{x} = γ≈ₑσ x

我们通过对输入项进行归纳来证明。

  • 如果它是 Z,那么我们使用前提 V ≈ N 立即得出结论。

  • 如果它是 S x,那么我们使用 subst-zero-exts 引理来重写,并使用前提 γ ≈ₑ σ 来得出结论。

为了证明主要的引理,我们需要另一个关于替换的技术性的引理。 接连应用两个替换与先将两个替换连接起来再应用等价。

sub-sub : ∀{Γ Δ Σ}{A}{M : Γ  A} {σ₁ : Subst Γ Δ}{σ₂ : Subst Δ Σ}
   subst σ₂ (subst σ₁ M)  subst (subst σ₂  σ₁) M
sub-sub {M = M} = plfa.part2.Substitution.sub-sub {M = M}

我们到达了主要引理:如果 M 在环境 γ 中大步求值为闭包 V,并且 γ ≈ₑ σ, 那么 subst σ M 将规约为某个等价于 V 的项 N。我们如下叙述该证明。

⇓→—↠×≈ : ∀{Γ}{γ : ClosEnv Γ}{σ : Subst Γ }{M : Γ  }{V : Clos}
        γ  M  V    γ ≈ₑ σ
         ---------------------------------------
        Σ[ N     ] (subst σ M —↠ N) × V  N
⇓→—↠×≈ {γ = γ} (⇓-var{x = x} γx≡Lδ δ⊢L⇓V) γ≈ₑσ
    with γ x | γ≈ₑσ x | γx≡Lδ
... | clos L δ |  τ ,  δ≈ₑτ , σx≡τL   | refl
      with ⇓→—↠×≈{σ = τ} δ⊢L⇓V δ≈ₑτ
...   |  N ,  τL—↠N , V≈N   rewrite σx≡τL =
         N ,  τL—↠N , V≈N  
⇓→—↠×≈ {σ = σ} {V = clos (ƛ N) γ} (⇓-lam) γ≈ₑσ =
     subst σ (ƛ N) ,  subst σ (ƛ N)  ,  σ ,  γ≈ₑσ , refl    
⇓→—↠×≈{Γ}{γ} {σ = σ} {L · M} {V} (⇓-app {N = N} L⇓ƛNδ N⇓V) γ≈ₑσ
    with ⇓→—↠×≈{σ = σ} L⇓ƛNδ γ≈ₑσ
... |  _ ,  σL—↠ƛτN ,  τ ,  δ≈ₑτ , ≡ƛτN     rewrite ≡ƛτN
      with ⇓→—↠×≈ {σ = ext-subst τ (subst σ M)} N⇓V
              x  ≈ₑ-ext{σ = τ} δ≈ₑτ  σ ,  γ≈ₑσ , refl   x)
           | β{}{subst (exts τ) N}{subst σ M}
...   |  N' ,  —↠N' , V≈N'   | ƛτN·σM—→
        rewrite sub-sub{M = N}{σ₁ = exts τ}{σ₂ = subst-zero (subst σ M)} =
        let rs = (ƛ subst (exts τ) N) · subst σ M —→⟨ ƛτN·σM—→  —↠N' in
        let g = —↠-trans (appL-cong σL—↠ƛτN) rs in
         N' ,  g , V≈N'  

该证明对 γ ⊢ M ⇓ V 进行归纳。我们有三种情况需要考虑。

  • 情况 ⇓-var: 此时我们有 γ x ≡ clos L δδ ⊢ L ⇓ V。 我们需要证明对于某个 Nsubst σ x —↠ NV ≈ N。 前提 γ ≈ₑ σ 告诉我们 γ x ≈ σ x,所以有 clos L δ ≈ σ x。 根据 的定义, 存在一个 τ 使得 δ ≈ₑ τσ x ≡ subst τ L。 使用 δ ⊢ L ⇓ Vδ ≈ₑ τ, 归纳假设使得对于某个 Nsubst τ L —↠ NV ≈ N。 所以我们证明了对于某个 N,有 subst σ x —↠ NV ≈ N
  • 情况 ⇓-lam: 我们立刻获得 subst σ (ƛ N) —↠ subst σ (ƛ N)clos (subst σ (ƛ N)) γ ≈ subst σ (ƛ N)
  • 情况 ⇓-app: 使用 γ ⊢ L ⇓ clos N δγ ≈ₑ σ, 归纳假设给我们

      subst σ L —↠ ƛ subst (exts τ) N                                     (1)

    并且对于某个 τδ ≈ₑ τ。 根据 γ≈ₑσ 我们有 clos M γ ≈ subst σ M。 与 (δ ,' clos M γ) ⊢ N ⇓ V 一同, 归纳假设给我们 V ≈ N'

      subst (subst (subst-zero (subst σ M)) ∘ (exts τ)) N —↠ N'         (2)

    同时根据 β,我们有

      (ƛ subst (exts τ) N) · subst σ M
      —→ subst (subst-zero (subst σ M)) (subst (exts τ) N)

    通过 sub-sub,这等价于

      (ƛ subst (exts τ) N) · subst σ M
      —→ subst (subst (subst-zero (subst σ M)) ∘ exts τ) N              (3)

    使用 (3) 和 (2) 我们有

      (ƛ subst (exts τ) N) · subst σ M —↠ N'                             (4)

    根据 (1) 我们有

      subst σ L · subst σ M —↠ (ƛ subst (exts τ) N) · subst σ M

    与 (4) 相结合,我们得出结论

      subst σ L · subst σ M —↠ N'

证明了主要引理后,我们便建立起大步语义与 β-规约等价关系的必要性。

cbn→reduce :  ∀{M :   }{Δ}{δ : ClosEnv Δ}{N′ : Δ ,   }
   ∅'  M  clos (ƛ N′) δ
    -----------------------------
   Σ[ N   ,    ] (M —↠ ƛ N)
cbn→reduce {M}{Δ}{δ}{N′} M⇓c
    with ⇓→—↠×≈{σ = ids} M⇓c ≈ₑ-id
... |  N ,  rs ,  σ ,  h , eq2     rewrite sub-id{M = M} | eq2 =
       subst (exts σ) N′ , rs 
练习 big-alt-implies-multi(实践)

为使用替换而不是环境的传名调用表达另一种大步语义,形式为 M ↓ N。 即应用规则 ⇓-app 应像在 N [ M ] 中一样执行替换而不是用 M 扩展环境。 证明 M ↓ N 蕴含 M —↠ N

-- 请将代码写在此处。

β-规约至 λ-抽象蕴含大步求值

充分性的证明,也就是 β-规约至 λ-抽象蕴含大步语义求值是更困难的。 困难源于通过 ζ 规则在 λ-抽象下的规约过程。 传名调用语义在 λ-演算中并不会规约,因此直接通过归纳规约序列来证明是不可能的。 在文章 Call-by-name, call-by-value, and the λ-calculus 中, Plotkin使用两个辅助规约关系分两步完成了证明。 第一步使用了 Curry-Feys 标准化这一经典方法, 它依赖于 标准规约序列(Standard Reduction Sequence) 的概念, 通过在 λ-演算下将传名调用扩展以包括规约, 标准规约序列充当了完整 β-规约与传名调用求值的中间点。 Plotkin证明了 M 能被规约为 L 当且仅当 ML 通过一个标准规约序列相关。

定理 1(标准化)
`M —↠ L` 当且仅当 `M` 能通过一个标准规约序列规约成 `L`。

Plotkin 接着引入了左规约(Left Reduction) 作为传名调用的小步描述, 并且用上方的定理证明了 β-规约与左规约在下述情况下等价。

推论 1
`M —↠ ƛ N` 当且仅当对于某个 `N′`,`M`能通过左规约成 `ƛ N′`。

证明的下一步将左规约与传名调用求值联系起来。

定理 2
`M` 左规约成 `ƛ N` 当且仅当 `⊢ M ⇓ ƛ N`。

(Plotkin 的传名调用求值使用替换而不是环境, 这解释了为何在上文定理的描述中环境被隐去了。)

将推论 1 和定理 2 相结合,Plotkin 证明了传名调用求值与 β-规约等价。

推论 2
`M —↠ ƛ N` 当且仅当对某个 `N′`,`⊢ M ⇓ ƛ N′`。

Plotkin 还证明了 λᵥ-演算中的类似结果,将其与传值调用求值相联系。 为了更好阐述该证明,我们推荐阅读由 Felleisen、Findler 和 Flatt 所著的 Semantics Engineering with PLT Redex 的第五章。

我们不通过上文描述的标准化方式来完成充分性的证明, 而是将其推迟到发展出 λ-演算的指称语义后, 此时该证明是指称语义中 soundness 和 adequacy 的推论。

Unicode

本章中使用了以下 Unicode:

≈  U+2248  ALMOST EQUAL TO (\~~ or \approx)
ₑ  U+2091  LATIN SUBSCRIPT SMALL LETTER E (\_e)
⊢  U+22A2  RIGHT TACK (\|- or \vdash)
⇓  U+21DB  DOWNWARDS DOUBLE ARROW (\d= or \Downarrow)

第三分册:指称语义

Denotational: 无类型 λ-演算的指称语义

module plfa.part3.Denotational where

λ-演算是一种关于函数的语言,即从输入到输出的映射。在计算中, 我们通常认为这种映射是通过一系列将输入转换为输出的操作来执行的。 但函数也可以表示为数据。例如,可以将函数表格化,即创建一个表, 其中每行都有两个条目:一个输入和一个该函数对应的输出。 函数应用则是查找给定输入的行并读取输出的过程。

我们可按照「函数即表格」的思想为无类型 λ-演算创建语义。然而却碰上了两个难点: 首先,函数的定义域通常是无穷的,因此我们似乎需要无限长的表格来表示函数。 其次,在 λ-演算中,函数可应用于函数,它们甚至可以应用于自身! 因而这些表格可能包含循环引用。你可能会担心需要高级技术来解决这些问题, 幸而情况并非如此!

第一个问题是带有无穷定义域的函数。注意到每一个 λ-抽象只会应用于数量有限的不同参数, 该问题因而得以解决(我们回头再讨论发散的程序)。这是看待 Dana Scott 见解的另一种方式,即只需要对 λ-演算进行建模只需要用到连续函数。

第二个问题是自应用,可以通过放宽在函数表格中查找参数的方式来解决。 通常,人们会在表中查找输入条目与参数完全匹配的行。在自应用的情况下, 这样会要求表包含其自身的副本,当然这是不可能的 (至少,想要使用归纳数据类型定义来构建表是不可能的,而这就是我们要做的)。 其实只要找到一个输入,使得每一行输入都对应到一行参数(即,输入是参数的子集)。 在自应用的情况下,表只需要包含其自身的较小副本,这样就好了。

基于这两点观察,我们就能直接写出 λ-演算的指称语义。

导入

open import Agda.Primitive using (lzero; lsuc)
open import Data.Nat using (; zero; suc)
open import Data.Product using (_×_; Σ; Σ-syntax; ; ∃-syntax; proj₁; proj₂)
  renaming (_,_ to ⟨_,_⟩)
open import Data.Sum
open import Data.Vec using (Vec; []; _∷_)
open import Relation.Binary.PropositionalEquality
  using (_≡_; _≢_; refl; sym; cong; cong₂; cong-app)
open import Relation.Nullary using (¬_)
open import Relation.Nullary.Negation using (contradiction)
open import Function using (_∘_)
open import plfa.part2.Untyped
  using (Context; ; _∋_; ; _,_; Z; S_; _⊢_; `_; _·_; ƛ_;
         #_; twoᶜ; ext; rename; exts; subst; subst-zero; _[_])
open import plfa.part2.Substitution using (Rename; extensionality; rename-id)

值数据类型 Value 表示函数的有限的一部分。我们将值视为表示「输入-输出」 映射的有限序对集合。Value 数据类型将集合表示为二叉树,其内部节点是并集运算符, 叶子节点表示单个映射或空集。

  • ⊥ 值不提供有关计算的信息。

  • 形如 v ↦ w 的值是从输入 v 到输出 w 的单个输入-输出映射。

  • 形如 v ⊔ w 的值是根据 vw 将输入映射到输出的函数。 可将其视为两个集合的并集。

infixr 7 _↦_
infixl 5 _⊔_

data Value : Set where
   : Value
  _↦_ : Value  Value  Value
  _⊔_ : Value  Value  Value

关系 将熟悉的子集概念适配到 Value 数据类型上。这种关系在实现自我应用方面 起到了关键作用。有两个特定于函数的规则 ⊑-fun⊑-dist,我们将在后面讨论。

infix 4 _⊑_

data _⊑_ : Value  Value  Set where

  ⊑-bot :  {v}    v

  ⊑-conj-L :  {u v w}
     v  u
     w  u
      -----------
     (v  w)  u

  ⊑-conj-R1 :  {u v w}
     u  v
      -----------
     u  (v  w)

  ⊑-conj-R2 :  {u v w}
     u  w
      -----------
     u  (v  w)

  ⊑-trans :  {u v w}
     u  v
     v  w
      -----
     u  w

  ⊑-fun :  {v w v′ w′}
     v′  v
     w  w′
      -------------------
     (v  w)  (v′  w′)

  ⊑-dist : ∀{v w w′}
      ---------------------------------
     v  (w  w′)  (v  w)  (v  w′)

前五条规则很简单。规则 ⊑-fun 刻画了何时可以将高阶参数 v′ ↦ w′ 与输入为 v ↦ w 的表格条目相匹配。考虑一个高阶参数的调用,可以传入一个比预期更大的参数, 因此 v 可以大于 v′。此外,还可以忽略某些输出,因此 w 可以小于 w′。 (译注:即作为子类型的函数,其参数是逆变的,返回值是协变的。) 规则 ⊑-dist 表示,如果对同一个输入有两个条目相匹配, 则可以将它们合并成一个条目并连接两个输出。

关系满足自反性:

⊑-refl :  {v}  v  v
⊑-refl {} = ⊑-bot
⊑-refl {v  v′} = ⊑-fun ⊑-refl ⊑-refl
⊑-refl {v₁  v₂} = ⊑-conj-L (⊑-conj-R1 ⊑-refl) (⊑-conj-R2 ⊑-refl)

运算对 满足单调性,即给定两个较大的值,它会产生一个更大的值:

⊔⊑⊔ :  {v w v′ w′}
   v  v′    w  w′
    -----------------------
   (v  w)  (v′  w′)
⊔⊑⊔ d₁ d₂ = ⊑-conj-L (⊑-conj-R1 d₁) (⊑-conj-R2 d₂)

即使输入的值不相同,⊑-dist 规则也可用于合并两个条目。首先可以用 将两个输入合并起来,然后应用 ⊑-dist 规则来获得以下属性。

⊔↦⊔-dist : ∀{v v′ w w′ : Value}
   (v  v′)  (w  w′)  (v  w)  (v′  w′)
⊔↦⊔-dist = ⊑-trans ⊑-dist (⊔⊑⊔ (⊑-fun (⊑-conj-R1 ⊑-refl) ⊑-refl)
                            (⊑-fun (⊑-conj-R2 ⊑-refl) ⊑-refl))

如果连接 u ⊔ v 小于另一个值 w,则 uv 都小于 w

⊔⊑-invL : ∀{u v w : Value}
   u  v  w
    ---------
   u  w
⊔⊑-invL (⊑-conj-L lt1 lt2) = lt1
⊔⊑-invL (⊑-conj-R1 lt) = ⊑-conj-R1 (⊔⊑-invL lt)
⊔⊑-invL (⊑-conj-R2 lt) = ⊑-conj-R2 (⊔⊑-invL lt)
⊔⊑-invL (⊑-trans lt1 lt2) = ⊑-trans (⊔⊑-invL lt1) lt2

⊔⊑-invR : ∀{u v w : Value}
   u  v  w
    ---------
   v  w
⊔⊑-invR (⊑-conj-L lt1 lt2) = lt2
⊔⊑-invR (⊑-conj-R1 lt) = ⊑-conj-R1 (⊔⊑-invR lt)
⊔⊑-invR (⊑-conj-R2 lt) = ⊑-conj-R2 (⊔⊑-invR lt)
⊔⊑-invR (⊑-trans lt1 lt2) = ⊑-trans (⊔⊑-invR lt1) lt2

环境

环境通过将变量映射到值来为项中的自由变量赋予含义:

Env : Context  Set
Env Γ =  (x : Γ  )  Value

我们有空环境,且可以扩展环境:

`∅ : Env 
`∅ ()

infixl 5 _`,_

_`,_ :  {Γ}  Env Γ  Value  Env (Γ , )
(γ `, v) Z = v
(γ `, v) (S x) = γ x

我们可以从扩展的环境中恢复以前的环境以及最后添加的值。 将它们再次合并在一起能让我们回到开始:

init :  {Γ}  Env (Γ , )  Env Γ
init γ x = γ (S x)

last :  {Γ}  Env (Γ , )  Value
last γ = γ Z

init-last :  {Γ}  (γ : Env (Γ , ))  γ  (init γ `, last γ)
init-last {Γ} γ = extensionality lemma
  where lemma :  (x : Γ ,   )  γ x  (init γ `, last γ) x
        lemma Z      =  refl
        lemma (S x)  =  refl

我们将 关系逐点(Point-wise)扩展到具有以下定义的环境:

_`⊑_ :  {Γ}  Env Γ  Env Γ  Set
_`⊑_ {Γ} γ δ =  (x : Γ  )  γ x  δ x

我们定义了一个底环境和一个环境上的连接运算符,它接受它们的值的逐点连接:

`⊥ :  {Γ}  Env Γ
`⊥ x = 

_`⊔_ :  {Γ}  Env Γ  Env Γ  Env Γ
(γ `⊔ δ) x = γ x  δ x

⊑-refl⊑-conj-R1⊑-conj-R2 规则对环境也适用,因此两个环境 γδ 的连接大于第一个环境 γ 或第二个环境 δ

`⊑-refl :  {Γ} {γ : Env Γ}  γ `⊑ γ
`⊑-refl {Γ} {γ} x = ⊑-refl {γ x}

⊑-env-conj-R1 :  {Γ}  (γ : Env Γ)  (δ : Env Γ)  γ `⊑ (γ `⊔ δ)
⊑-env-conj-R1 γ δ x = ⊑-conj-R1 ⊑-refl

⊑-env-conj-R2 :  {Γ}  (γ : Env Γ)  (δ : Env Γ)  δ `⊑ (γ `⊔ δ)
⊑-env-conj-R2 γ δ x = ⊑-conj-R2 ⊑-refl

指称语义

我们用形如 ρ ⊢ M ↓ v 的判断来定义语义,其中 ρ 是环境,M 程序,v 是结果值。 对于熟悉大步语义的读者来说,这种表示法会感觉很自然,但不要让这种相似性欺骗了你。 二者之间存在细微但重要的差异!下面是语义的定义,我们将在后面的段落中详细讨论:

infix 3 _⊢_↓_

data _⊢_↓_ : ∀{Γ}  Env Γ  (Γ  )  Value  Set where

  var :  {Γ} {γ : Env Γ} {x}
      ---------------
     γ  (` x)  γ x

  ↦-elim :  {Γ} {γ : Env Γ} {L M v w}
     γ  L  (v  w)
     γ  M  v
      ---------------
     γ  (L · M)  w

  ↦-intro :  {Γ} {γ : Env Γ} {N v w}
     γ `, v  N  w
      -------------------
     γ  (ƛ N)  (v  w)

  ⊥-intro :  {Γ} {γ : Env Γ} {M}
      ---------
     γ  M  

  ⊔-intro :  {Γ} {γ : Env Γ} {M v w}
     γ  M  v
     γ  M  w
      ---------------
     γ  M  (v  w)

  sub :  {Γ} {γ : Env Γ} {M v w}
     γ  M  v
     w  v
      ---------
     γ  M  w

考虑 λ-抽象的规则 ↦-intro,它表示 λ-抽象会生成一个单条目的表,该表将输入 v 映射到输出 w,前提是在具有 v 绑定到其形参会产生输出 w。 作为此规则的简单示例,我们可以看到恒等函数将 映射到 ,并将 映射到 ⊥ ↦ ⊥

id :   
id = ƛ # 0
denot-id1 :  {γ}  γ  id    
denot-id1 = ↦-intro var

denot-id2 :  {γ}  γ  id  (  )  (  )
denot-id2 = ↦-intro var

当然,我们需要多行表格来刻画 λ-抽象的含义。它们可以用 ⊔-intro 规则构建。如果项 M(通常是一个 λ-抽象)可以产生表格 vw, 那么它也可以产生合并的表格 v ⊔ w。我们可以从操作的视角看待规则 ↦-intro⊔-intro。想象一下,当解释器首次遇到 λ-抽象时, 它会在许多随机选择的参数上预先对函数求值,使用多个规则 ↦-intro 的实例,然后使用多个规则 ⊔-intro 将它们合并成一个大表格。 在接下来的内容中,我们将展示恒等函数产生一个包含上述两个结果的表格 ⊥ ↦ ⊥(⊥ ↦ ⊥) ↦ (⊥ ↦ ⊥)

denot-id3 : `∅  id  (  )  (  )  (  )
denot-id3 = ⊔-intro denot-id1 denot-id2

我们通常认为判断 γ ⊢ M ↓ v 以环境 γ 和项 M 为输入,产生结果 v。 然而重点在于,语义是一种关系。上述恒等函数的结果表明, 相同的环境和项可以映射为不同的结果。然而,对于给定的 γM ,它们的结果并不会有太大区别,毕竟它们都是同一个函数的有限近似。 或许考虑判断 γ ⊢ M ↓ v 更好的方法是将 γMv 都视为输入, 其语义则是判定 v 是否为精确的环境 γM 的求值结果的部分描述。

接下来我们考虑 ↦-elim 规则给出的函数应用的含义。 以此规则为前提,我们有规则 Lv 映射到 w。 因此,如果 M 产生 v,那么将 L 应用于 M 会产生 w

举一个函数应用和 ↦-elim 规则的例子,我们将恒等函数应用于自身。 实际上,我们有 ∅ ⊢ id ↓ (u ↦ u) ↦ (u ↦ u) 的同时还有 ∅ ⊢ id ↓ (u ↦ u),因此我们可以应用规则 ↦-elim

id-app-id :  {u : Value}  `∅  id · id  (u  u)
id-app-id {u} = ↦-elim (↦-intro var) (↦-intro var)

接着我们重新考虑丘奇数二:λ f. λ u. (f (f u))。该函数有两个参数:f 和一个任意值 u,并将 f 应用两次。于是 f 必定将 u 映射到某个值, 我们将其命名为 v。接着,对于第二次应用,f 必定将 v 映射到某个值, 我们将其命名为 w。因此该函数的表格必定包含两个条目,即 u ↦ vv ↦ w。 对于该表格的每一次应用,我们用 sub 规则提取对应的条目。具体来说, 就是用 ⊑-conj-R1⊑-conj-R2 从表格 u ↦ v ⊔ v ↦ w 中分别选出 u ↦ vv ↦ w。所以 twoᶜ 的涵义就是接受该表格和参数 u,然后返回 w。 实际上我们通过以下过程把它推导出来的:

denot-twoᶜ : ∀{u v w : Value}  `∅  twoᶜ  ((u  v  v  w)  u  w)
denot-twoᶜ {u}{v}{w} =
  ↦-intro (↦-intro (↦-elim (sub var lt1) (↦-elim (sub var lt2) var)))
  where lt1 : v  w  u  v  v  w
        lt1 = ⊑-conj-R2 (⊑-fun ⊑-refl ⊑-refl)

        lt2 : u  v  u  v  v  w
        lt2 = (⊑-conj-R1 (⊑-fun ⊑-refl ⊑-refl))

接下来展示一个自应用的经典例子:Δ = λx. (x x)x 的输入值必须是一个表格,其中有一个条目将其较小的版本 v 映射到某个值 w,所以输入值类似于 v ↦ w ⊔ v。当然, Δ 的输出就是 w,推导过程如下所示。第一个 x 求值为 v ↦ w, 第二个 x 求值为 v,应用的结果为 w

Δ :   
Δ = (ƛ (# 0) · (# 0))

denot-Δ :  {v w}  `∅  Δ  ((v  w  v)  w)
denot-Δ = ↦-intro (↦-elim (sub var (⊑-conj-R1 ⊑-refl))
                          (sub var (⊑-conj-R2 ⊑-refl)))

你可能会担心这种语义是否可以处理发散的程序。值 和规则 ⊥-intro 提供了一种处理它们的方法(⊥-intro 规则也是 β-规约能够应用于不停机参数的原因)。 经典的 Ω 程序是一个特别简单的发散程序,它将 Δ 应用于自身,语义赋予 Ω 含义 。有多种方法可以得出它,我们将从使用 ⊔-intro 规则的方法开始。 首先,denot-Δ 告诉我们 Δ 的计算结果为 ((⊥ ↦ ⊥) ⊔ ⊥) ↦ ⊥(选择 v₁ = v2 = ⊥ )。接着,Δ 还通过使用 ↦-intro⊥-intro 求值为 ⊥ ↦ ⊥,并通过 ⊥-intro 求值为 。正如我们之前所看到的, 只要我们能证明程序会求值出两个值,我们就能应用 ⊔-intro 将它们连接在一起, 于是 Δ 求值为 (⊥ ↦ ⊥) ⊔ ⊥,这与第一个 Δ 的输入相匹配, 因此可得应用的结果是

Ω :   
Ω = Δ · Δ

denot-Ω : `∅  Ω  
denot-Ω = ↦-elim denot-Δ (⊔-intro (↦-intro ⊥-intro) ⊥-intro)

同一结果的较短推导就是单纯使用 ⊥-intro 规则:

denot-Ω' : `∅  Ω  
denot-Ω' = ⊥-intro

仅凭对某个封闭项 M 可以推导出 ∅ ⊢ M ↓ ⊥ 并不意味着 M 必然发散。 可能还有其他可以推出 M 的推导过程能产生包含更多信息的值。然而, 如果一个项求值的唯一结果是 ,那么它确实发散。

细心的读者会发现,我们计划解决自应用问题的方式与实际应用的 ↦-elim 规则之间存在脱节。开头说过,我们会放宽查表的概念, 如果参数等于或大于输入条目,则允许参数匹配输入条目。然而,↦-elim 规则似乎需要精确匹配,但由于存在 sub 规则,应用确实可以允许更大的参数。

↦-elim2 :  {Γ} {γ : Env Γ} {M₁ M₂ v₁ v₂ v₃}
   γ  M₁  (v₁  v₃)
   γ  M₂  v₂
   v₁  v₂
    ------------------
   γ  (M₁ · M₂)  v₃
↦-elim2 d₁ d₂ lt = ↦-elim d₁ (sub d₂ lt)
练习 denot-plusᶜ(实践)

plusᶜ 的指称是什么?即,找到一个值 v(排除 ),使得 ∅ ⊢ plusᶜ ↓ v。 此外,对你选择的 v 给出 ∅ ⊢ plusᶜ ↓ v 的证明。

-- 请将代码写在此处

指称与指称相等

接下来我们基于上述语义来定义指称相等的概念,它的语句使用了「当且仅当」, 其定义如下

_iff_ : Set  Set  Set
P iff Q = (P  Q) × (Q  P)

看待指称语义的另一种方式是将它看作一个函数,它将项映射为一个从环境到值的关系, 即,项的指称(Denotation)是一个从环境到值的关系。

Denotation : Context  Set₁
Denotation Γ = (Env Γ  Value  Set)

下面的函数 ℰ 给出了语义的另一种视角,它相当于只改变了参数的顺序:

 : ∀{Γ}  (M : Γ  )  Denotation Γ
 M = λ γ v  γ  M  v

一般来说,当两个指称在相同的环境中能够产生相同的值时,二者就是相等的。

infix 3 _≃_

_≃_ :  {Γ}  (Denotation Γ)  (Denotation Γ)  Set
(_≃_ {Γ} D₁ D₂) = (γ : Env Γ)  (v : Value)  D₁ γ v iff D₂ γ v

指称相等是一种等价关系:

≃-refl :  {Γ : Context}  {M : Denotation Γ}
   M  M
≃-refl γ v =   x  x) ,  x  x) 

≃-sym :  {Γ : Context}  {M N : Denotation Γ}
   M  N
    -----
   N  M
≃-sym eq γ v =  (proj₂ (eq γ v)) , (proj₁ (eq γ v)) 

≃-trans :  {Γ : Context}  {M₁ M₂ M₃ : Denotation Γ}
   M₁  M₂
   M₂  M₃
    -------
   M₁  M₃
≃-trans eq1 eq2 γ v =   z  proj₁ (eq2 γ v) (proj₁ (eq1 γ v) z)) ,
                         z  proj₂ (eq1 γ v) (proj₂ (eq2 γ v) z)) 

若两个项 MN 的指称相等,我们就说它们是指称相等的,即 ℰ M ≃ ℰ N

以下子模块引入了 关系的等式推理:

module ≃-Reasoning {Γ : Context} where

  infix  1 start_
  infixr 2 _≃⟨⟩_ _≃⟨_⟩_
  infix  3 _☐

  start_ :  {x y : Denotation Γ}
     x  y
      -----
     x  y
  start x≃y  =  x≃y

  _≃⟨_⟩_ :  (x : Denotation Γ) {y z : Denotation Γ}
     x  y
     y  z
      -----
     x  z
  (x ≃⟨ x≃y  y≃z) =  ≃-trans x≃y y≃z

  _≃⟨⟩_ :  (x : Denotation Γ) {y : Denotation Γ}
     x  y
      -----
     x  y
  x ≃⟨⟩ x≃y  =  x≃y

  _☐ :  (x : Denotation Γ)
      -----
     x  x
  (x )  =  ≃-refl

后续章节的路线图

后续章节证明了指称语义拥有几个期望的性质。首先我们证明了语义是可组合的, 即,一个项的指称是其子项的指称的函数。为此我们需要证明以下形式的等式:

ℰ (` x) ≃ ...
ℰ (ƛ M) ≃ ... ℰ M ...
ℰ (M · N) ≃ ... ℰ M ... ℰ N ...

组合性的性质并不平凡,因为我们定义的语义包含三个非语法制导的规则: ⊥-intro⊔-introsub。上面的等式表明指称语义可以被定义为递归函数, 实际上,我们确实给出了这样的定义并证明它等价于 ℰ。

接下来我们研究指称语义和规约语义是否等价。回想一下,一个语言的语义的作用, 就是描述给定程序 M 的可观测行为。对于 λ-演算,我们可以做出多种选择, 但它们通常可以归结为一点信息:

  • 发散:程序 M 永远执行。
  • 停机:程序 M 停止。

我们可以用规约的项来刻画发散和停机:

  • 发散:对于任意项 N,有 ¬ (M —↠ ƛ N)
  • 停机:对于某个项 N,有 M —↠ ƛ N

我们也可以用指称来刻画发散和停机:

  • 发散:对于任意 vw,有 ¬ (∅ ⊢ M ↓ v ↦ w)
  • 停机:对于某个 vw,有 ∅ ⊢ M ↓ v ↦ w

此外,我们还可以用指称函数

  • 发散:对于任意项 N,有 ¬ (ℰ M ≃ ℰ (ƛ N))
  • 停机:对于某个项 N,有 ℰ M ≃ ℰ (ƛ N)

所以问题在于规约语义和指称语义是否等价:

(∃ N. M —↠ ƛ N)  当且仅当  (∃ N. ℰ M ≃ ℰ (ƛ N))

我们将在第二章和第三章中讨论等价的每个方向。在第二章中,我们证明了 λ-抽象的规约蕴含 λ-抽象的指称相等。此性质在文献中被称为可靠性(Soundness)

M —↠ ƛ N  蕴含  ℰ M ≃ ℰ (ƛ N)

在第三章中,我们证明了 λ-抽象的指称相等蕴含 λ-抽象的规约。 此性质在文献中被称为充分性(Adequacy)

ℰ M ≃ ℰ (ƛ N)  蕴含 M —↠ ƛ N′ 对于某个 N′

第四章应用前三章的结果(组合性、可靠性和充分性)来证明指称相等蕴含一种称为 语境等价(Contextual Equivalence)的性质。这个性质很重要, 因为它保证了在证明程序变换(如性能优化)的正确性时使用指称相等的合理性。

我们会在本章剩下的部分中建立关于指称语义的一些基本结果, 所有这些性质的证明都依赖此。我们先从一些关于重命名的引理开始, 它们与我们在前面的章节中见过的重命名引理非常相似。 我们以关于函数值的小于关系的重要反演引理的证明作为结论。

重命名保持指称不变

我们将证明重命名变量并更改环境中对应的项仍能保持项的含义。 我们推广了重命名引理,以允许新环境中的值等于或大于原始值, 这种推广有助于证明规约蕴含指称相等。

和以前一样,我们需要一个扩展引理来处理在 λ-抽象的情况下的证明。假设 ρ 是一个重命名,它将 γ 中的变量映射为 δ 中具有相等或更大值的变量。 这个引理说明了,扩展重命名会产生一个重命名 ext r,它将 γ , v 映射到 δ , v

ext-⊑ :  {Γ Δ v} {γ : Env Γ} {δ : Env Δ}
   (ρ : Rename Γ Δ)
   γ `⊑ (δ  ρ)
    ------------------------------
   (γ `, v) `⊑ ((δ `, v)  ext ρ)
ext-⊑ ρ lt Z = ⊑-refl
ext-⊑ ρ lt (S n′) = lt n′

我们对 de Bruijn 索引 n 分情况来证明:

  • 若索引为 Z,那么我们只需证明 v ⊑ v,它根据 ⊑-refl 成立。

  • 若索引为 S n′,则目标简化为 γ n′ ⊑ δ (ρ n′),它是前提的一个实例。

现在是重命名引理的证明。假设我们有一个重命名,它将 γ 中的变量映射为 δ 中具有相同值的变量。若在环境 γM 求值为 v,则对 M 应用重命名会生成一个程序,它在环境 δ 中求值会产生相同的值 v

rename-pres :  {Γ Δ v} {γ : Env Γ} {δ : Env Δ} {M : Γ  }
   (ρ : Rename Γ Δ)
   γ `⊑ (δ  ρ)
   γ  M  v
    ---------------------
   δ  (rename ρ M)  v
rename-pres ρ lt (var {x = x}) = sub var (lt x)
rename-pres ρ lt (↦-elim d d₁) =
   ↦-elim (rename-pres ρ lt d) (rename-pres ρ lt d₁)
rename-pres ρ lt (↦-intro d) =
   ↦-intro (rename-pres (ext ρ) (ext-⊑ ρ lt) d)
rename-pres ρ lt ⊥-intro = ⊥-intro
rename-pres ρ lt (⊔-intro d d₁) =
   ⊔-intro (rename-pres ρ lt d) (rename-pres ρ lt d₁)
rename-pres ρ lt (sub d lt′) =
   sub (rename-pres ρ lt d) lt′

证明通过对 M 的语义进行归纳而得出。如你所见,除了变量和 λ-的情况外,其他情况都很简单。

  • 对于 x,我们根据前提来证明 γ x ⊑ δ (ρ x)

  • 对于 λ-抽象,归纳假设需要我们扩展重命名。我们扩展了它并使用 ext-⊑ 引理来证明,扩展重命名会将变量映射到具有等价的值的变量。

环境增强与恒等重命名

我们还需要一个重命名引理的推论,即用更大(或更强)的环境替换当前环境, 并不会改变项 M 是否产生特定的值 v。具体来说,即若 γ ⊢ M ↓ vγ ⊑ δ,则 δ ⊢ M ↓ v。那么这和重命名有何关联呢? 它其实就是用恒等函数来重命名。我们以恒等重命名来应用重命名引理, 会得到 δ ⊢ rename (λ {A} x → x) M ↓ v,之后应用 rename-id 引理就会得到 δ ⊢ M ↓ v

⊑-env :  {Γ} {γ : Env Γ} {δ : Env Γ} {M v}
   γ  M  v
   γ `⊑ δ
    ----------
   δ  M  v
⊑-env{Γ}{γ}{δ}{M}{v} d lt
      with rename-pres{Γ}{Γ}{v}{γ}{δ}{M}  {A} x  x) lt d
... | δ⊢id[M]↓v rewrite rename-id {Γ}{}{M} =
      δ⊢id[M]↓v

在代换反映了指称的证明中,对于 λ-抽象这一情况,我们使用 ⊑-env 的一个小变体,其中只有环境中的最后一个元素变大:

up-env :  {Γ} {γ : Env Γ} {M v u₁ u₂}
   (γ `, u₁)  M  v
   u₁  u₂
    -----------------
   (γ `, u₂)  M  v
up-env d lt = ⊑-env d (ext-le lt)
  where
  ext-le :  {γ u₁ u₂}  u₁  u₂  (γ `, u₁) `⊑ (γ `, u₂)
  ext-le lt Z = lt
  ext-le lt (S n) = ⊑-refl
练习 denot-church(推荐)

在路径的表达上,丘奇数比自然数更通用。一条路径由 n 条边和 n + 1 个顶点构成。 我们将顶点逆序存储在长度为 n + 1 的向量中。路径中的边将第 i 个顶点映射到第 i + 1 个顶点。以下函数 D^suc(表示后继(successor)的指称)构造了一个表, 其中的条目是路径上所有的边:

D^suc : (n : )  Vec Value (suc n)  Value
D^suc zero (a[0]  []) = 
D^suc (suc i) (a[i+1]  a[i]  ls) =  a[i]  a[i+1]    D^suc i (a[i]  ls)

我们用以下辅助函数来获取非空向量的第一个元素(就我们的目的而言,这种表示方式比 Agda 标准库中的更加方便)。

vec-last : ∀{n : }  Vec Value (suc n)  Value
vec-last {0} (a  []) = a
vec-last {suc n} (a  b  ls) = vec-last (b  ls)

函数 Dᶜ 计算给定的路径上第 n 个丘奇数的指称。

Dᶜ : (n : )  Vec Value (suc n)  Value
Dᶜ n (a[n]  ls) = (D^suc n (a[n]  ls))  (vec-last (a[n]  ls))  a[n]
  • 0 的丘奇数忽略其第一个参数并返回第二个参数,因此对于只由 a[0] 构成的单例路径,其指称为

      ⊥ ↦ a[0] ↦ a[0]
  • suc n 的丘奇数接受两个参数:一个后继函数,其指称由 D^suc 给定, 以及一个路径的起点(向量的最后一个元素)。它返回路径中的第 n + 1 个节点。

      (D^suc (suc n) (a[n+1] ∷ a[n] ∷ ls)) ↦ (vec-last (a[n] ∷ ls)) ↦ a[n+1]

此练习证明了对于任意路径 ls 邱奇数 n 的含义是 Dᶜ n ls

为了便于讨论任意丘奇数,以下 church 函数通过辅助函数 apply-n 来构建第 n 个丘奇数的项:

apply-n : (n : )   ,  ,   
apply-n zero = # 0
apply-n (suc n) = # 1 · apply-n n

church : (n : )    
church n = ƛ ƛ apply-n n

证明以下定理:

denot-church : ∀{n : ℕ}{ls : Vec Value (suc n)}
   → `∅ ⊢ church n ↓ Dᶜ n ls
-- 请将代码写在此处

函数小于关系的反演

已知函数 v ↦ w 小于某个值 u,我们可以推断出什么?关于 u 我们可以推断出什么?问题的答案被称为「函数小于」的反演性(Inversion)。 这个问题并不容易回答,因为 ⊑-dist 规则将左侧的函数与右侧的一对函数联系了起来。 因此,u 可能包含多个与 v ↦ w 相关的一组函数。此外,由于规则 ⊑-conj-R1⊑-conj-R2u 中可能还有别的值(如 )与 v ↦ w 无关。但总的来说, 我们可以推断出 u 包含了一组函数,其中它们的定义域的连接小于 v,而它们的陪域的连接大于 w

为了精确陈述并证明这种反演性,我们需要定义「一个值包含一组值的含义。 我们还需要定义如何计算它们的定义域和陪域的连接。

值的成员关系与包含关系

前面我们将值视作一个集合,它由条目以及类似并集的连接运算符 v ⊔ w 组成。 函数值 v ↦ w 和底值 构成了集合的两种元素(在其他语境下,人们可能将 视作空集,但在这里我们必须将它视为一个元素)。我们用 u ∈ v表示 uv 的一个元素,定义如下:

infix 5 _∈_

_∈_ : Value  Value  Set
u   = u  
u  v  w = u  v  w
u  (v  w) = u  v  u  w

于是我们可以简单地将一组值表示为一个值。我们用 v ⊆ w 表示 v 的所有元素也都属于 w

infix 5 _⊆_

_⊆_ : Value  Value  Set
v  w = ∀{u}  u  v  u  w

值的成员与包含关系的概念和小于关系密切相关, 它们是蕴含了小于关系的更狭义的关系,反之则不然。

∈→⊑ : ∀{u v : Value}
     u  v
      -----
     u  v
∈→⊑ {.} {} refl = ⊑-bot
∈→⊑ {v  w} {v  w} refl = ⊑-refl
∈→⊑ {u} {v  w} (inj₁ x) = ⊑-conj-R1 (∈→⊑ x)
∈→⊑ {u} {v  w} (inj₂ y) = ⊑-conj-R2 (∈→⊑ y)

⊆→⊑ : ∀{u v : Value}
     u  v
      -----
     u  v
⊆→⊑ {} s with s {} refl
... | x = ⊑-bot
⊆→⊑ {u  u′} s with s {u  u′} refl
... | x = ∈→⊑ x
⊆→⊑ {u  u′} s = ⊑-conj-L (⊆→⊑  z  s (inj₁ z))) (⊆→⊑  z  s (inj₂ z)))

我们还需要一些值的包含关系的反演原则。如果 uv 的并含于 w, 那么 uv 自然都含于 w

⊔⊆-inv : ∀{u v w : Value}
        (u  v)  w
         ---------------
        u  w  ×  v  w
⊔⊆-inv uvw =   x  uvw (inj₁ x)) ,  x  uvw (inj₂ x)) 

在我们的值的表示中,v ↦ w 既是一个元素,又是一个单例集。因此如果 v ↦ wu 的一个子集,那么 v ↦ w 必定是 u 的一个成员。

↦⊆→∈ : ∀{v w u : Value}
      v  w  u
       ---------
      v  w  u
↦⊆→∈ incl = incl refl

函数值

为了识别函数的集合,我们定义了以下两个谓词。如果 u 是一个函数值, 即对于某些值 vw,有 u ≡ v ↦ w,我们就记作 Fun u。 如果 v 中的所有元素都是函数,我们就记作 all-funs v

data Fun : Value  Set where
  fun : ∀{u v w}  u  (v  w)  Fun u

all-funs : Value  Set
all-funs v = ∀{u}  u  v  Fun u

不是函数:

¬Fun⊥ : ¬ (Fun )
¬Fun⊥ (fun ())

在「值作为集合」的表示中,集合总是包含至少一个元素。即,如果所有的元素都是函数, 那么至少存在一个函数。

all-funs∈ : ∀{u}
       all-funs u
       Σ[ v  Value ] Σ[ w  Value ] v  w  u
all-funs∈ {} f with f {} refl
... | fun ()
all-funs∈ {v  w} f =  v ,  w , refl  
all-funs∈ {u  u′} f
    with all-funs∈  z  f (inj₁ z))
... |  v ,  w , m   =  v ,  w , (inj₁ m)  

定义域与陪域

回到我们一开始的目标上来,即「小于一个函数」的反演法则。我们想要证明 v ↦ w ⊑ u 蕴含「u 包含一个函数值的集合,它们定义域的连接小于 v,陪域的连接大于 w」。

为此,我们定义了以下 ⨆dom⨆cod 函数。给定某个值 u(表示一个条目的集合), ⨆dom u 返回其定义域的连接,⨆cod u 返回其共域的连接。

⨆dom : (u : Value)  Value
⨆dom   = 
⨆dom (v  w) = v
⨆dom (u  u′) = ⨆dom u  ⨆dom u′

⨆cod : (u : Value)  Value
⨆cod   = 
⨆cod (v  w) = w
⨆cod (u  u′) = ⨆cod u  ⨆cod u′

对于 ⨆dom⨆cod 我们只需要一个性质。给定一个函数的的集合,表示为值 u,和一个条目 v ↦ w ∈ u,我们能够证明 v 包含在定义域 u 中:

↦∈→⊆⨆dom : ∀{u v w : Value}
           all-funs u    (v  w)  u
            ----------------------
           v  ⨆dom u
↦∈→⊆⨆dom {} fg () u∈v
↦∈→⊆⨆dom {v  w} fg refl u∈v = u∈v
↦∈→⊆⨆dom {u  u′} fg (inj₁ v↦w∈u) u∈v =
   let ih = ↦∈→⊆⨆dom  z  fg (inj₁ z)) v↦w∈u in
   inj₁ (ih u∈v)
↦∈→⊆⨆dom {u  u′} fg (inj₂ v↦w∈u′) u∈v =
   let ih = ↦∈→⊆⨆dom  z  fg (inj₂ z)) v↦w∈u′ in
   inj₂ (ih u∈v)

对于 ⨆cod,假设我们有一个函数集合 u,但它们都只是 v ↦ w 的副本。 那么 ⨆cod u 包含在 w 中:

⊆↦→⨆cod⊆ : ∀{u v w : Value}
         u  v  w
          ---------
         ⨆cod u  w
⊆↦→⨆cod⊆ {} s refl with s {} refl
... | ()
⊆↦→⨆cod⊆ {C  C′} s m with s {C  C′} refl
... | refl = m
⊆↦→⨆cod⊆ {u  u′} s (inj₁ x) = ⊆↦→⨆cod⊆  {C} z  s (inj₁ z)) x
⊆↦→⨆cod⊆ {u  u′} s (inj₂ y) = ⊆↦→⨆cod⊆  {C} z  s (inj₂ z)) y

有了 ⨆dom⨆cod 函数,我们就可以精确地得出函数的反演法则, 并将其封装到下面的谓词 factor 中。 如果 u′ 包含在 u 中, 我们就说 v ↦ wu 分解(factor)u′,如果 u′ 中只包含函数,那么其定义域小于 v,其陪域大于 w

factor : (u : Value)  (u′ : Value)  (v : Value)  (w : Value)  Set
factor u u′ v w = all-funs u′  ×  u′  u  ×  ⨆dom u′  v  ×  w  ⨆cod u′

因此函数的反演法则可被陈述为:

  v ↦ w ⊑ u
  ---------------
→ factor u u′ v w

我们通过在小于关系的推导上归纳证明了函数的反演法则。为了让归纳假设更强, 我们将前提 v ↦ w ⊑ u 推广为 u₁ ⊑ u₂ 并加强了结论,即对于每一个函数值 v ↦ w ∈ u₁,我们都有 v ↦ wu₂ 分解为某个值 u₃

→ u₁ ⊑ u₂
  ------------------------------------------------------
→ ∀{v w} → v ↦ w ∈ u₁ → Σ[ u₃ ∈ Value ] factor u₂ u₃ v w

函数小于的反演,⊑-trans 的情况

证明的核心在 ⊑-trans 的情况:

u₁ ⊑ u   u ⊑ u₂
--------------- (⊑-trans)
    u₁ ⊑ u₂

根据归纳假设 u₁ ⊑ u,我们证明了对某个值 u′,有 v ↦ w factors u into u′, 因此我们有 all-funs u′u′ ⊆ u。 根据归纳假设 u ⊑ u₂,我们证明了对于任意 v′ ↦ w′ ∈ uv′ ↦ w′u₂ 分解为 u₃。 有了这些事实,我们就能对 u′ 进行归纳,证明 (⨆dom u′) ↦ (⨆cod u′)u₂ 分解为 u₃。接下来我们讨论证明的每种情况。

sub-inv-trans : ∀{u′ u₂ u : Value}
     all-funs u′    u′  u
     (∀{v′ w′}  v′  w′  u  Σ[ u₃  Value ] factor u₂ u₃ v′ w′)
      ---------------------------------------------------------------
     Σ[ u₃  Value ] factor u₂ u₃ (⨆dom u′) (⨆cod u′)
sub-inv-trans {} {u₂} {u} fu′ u′⊆u IH =
   contradiction (fu′ refl) ¬Fun⊥
sub-inv-trans {u₁′  u₂′} {u₂} {u} fg u′⊆u IH = IH (↦⊆→∈ u′⊆u)
sub-inv-trans {u₁′  u₂′} {u₂} {u} fg u′⊆u IH
    with ⊔⊆-inv u′⊆u
... |  u₁′⊆u , u₂′⊆u 
    with sub-inv-trans {u₁′} {u₂} {u}  {v′} z  fg (inj₁ z)) u₁′⊆u IH
       | sub-inv-trans {u₂′} {u₂} {u}  {v′} z  fg (inj₂ z)) u₂′⊆u IH
... |  u₃₁ ,  fu21' ,  u₃₁⊆u₂ ,  du₃₁⊑du₁′ , cu₁′⊑cu₃₁    
    |  u₃₂ ,  fu22' ,  u₃₂⊆u₂ ,  du₃₂⊑du₂′ , cu₁′⊑cu₃₂     =
       (u₃₁  u₃₂) ,  fu₂′ ,  u₂′⊆u₂ ,
       ⊔⊑⊔ du₃₁⊑du₁′ du₃₂⊑du₂′ ,
        ⊔⊑⊔ cu₁′⊑cu₃₁ cu₁′⊑cu₃₂    
    where fu₂′ : {v′ : Value}  v′  u₃₁  v′  u₃₂  Fun v′
          fu₂′ {v′} (inj₁ x) = fu21' x
          fu₂′ {v′} (inj₂ y) = fu22' y
          u₂′⊆u₂ : {C : Value}  C  u₃₁  C  u₃₂  C  u₂
          u₂′⊆u₂ {C} (inj₁ x) = u₃₁⊆u₂ x
          u₂′⊆u₂ {C} (inj₂ y) = u₃₂⊆u₂ y
  • 假设 u′ ≡ ⊥,那么我们可导出矛盾,因为它不满足 Fun ⊥ 的情况。
  • 假设 u′ ≡ u₁′ ↦ u₂′,那么 u₁′ ↦ u₂′ ∈ u,且我们可以应用前提 (归纳假设 u ⊑ u₂)来得到 u₁′ ↦ u₂′u₂ 分解为 u₃。 此情况是完整的,因为 ⨆dom u′ ≡ u₁′⨆cod u′ ≡ u₂′
  • 假设 u′ ≡ u₁′ ⊔ u₂′,那么 u₁′ ⊆ uu₂′ ⊆ u。我们同样有 all-funs u₁′all-funs u₂′,于是我们可以对 u₁′u₂′ 应用归纳假设。因此存在值 u₃₁u₃₂ 使得 (⨆dom u₁′) ↦ (⨆cod u₁′)u 分解为 u₃₁,以及 (⨆dom u₂′) ↦ (⨆cod u₂′)u 分解为 u₃₂。 我们要证明 (⨆dom u) ↦ (⨆cod u)u 分解为 u₃₁ ⊔ u₃₂,因此我们需要证明

      ⨆dom (u₃₁ ⊔ u₃₂) ⊑ ⨆dom (u₁′ ⊔ u₂′)
      ⨆cod (u₁′ ⊔ u₂′) ⊑ ⨆cod (u₃₁ ⊔ u₃₂)

然而二者可直接根据 对于 的单调性,从 u 分解为 u₃₁u₃₂ 得到。

函数小于的反演

我们来证明有关函数小于的反演的主要引理。我们要证明若 u₁ ⊑ u₂, 则对于任何 v ↦ w ∈ u₁,都可以根据 v ↦ wu₂ 分解为 u₃。 我们对 u₁ ⊑ u₂ 的推导进行归纳,并在 Agda 证明过程后面描述证明中的每一种情况。

sub-inv : ∀{u₁ u₂ : Value}
         u₁  u₂
         ∀{v w}  v  w  u₁
          -------------------------------------
         Σ[ u₃  Value ] factor u₂ u₃ v w
sub-inv {} {u₂} ⊑-bot {v} {w} ()
sub-inv {u₁₁  u₁₂} {u₂} (⊑-conj-L lt1 lt2) {v} {w} (inj₁ x) = sub-inv lt1 x
sub-inv {u₁₁  u₁₂} {u₂} (⊑-conj-L lt1 lt2) {v} {w} (inj₂ y) = sub-inv lt2 y
sub-inv {u₁} {u₂₁  u₂₂} (⊑-conj-R1 lt) {v} {w} m
    with sub-inv lt m
... |  u₃₁ ,  fu₃₁ ,  u₃₁⊆u₂₁ ,  domu₃₁⊑v , w⊑codu₃₁     =
       u₃₁ ,  fu₃₁ ,   {w} z  inj₁ (u₃₁⊆u₂₁ z)) ,
                                    domu₃₁⊑v , w⊑codu₃₁    
sub-inv {u₁} {u₂₁  u₂₂} (⊑-conj-R2 lt) {v} {w} m
    with sub-inv lt m
... |  u₃₂ ,  fu₃₂ ,  u₃₂⊆u₂₂ ,  domu₃₂⊑v , w⊑codu₃₂     =
       u₃₂ ,  fu₃₂ ,   {C} z  inj₂ (u₃₂⊆u₂₂ z)) ,
                                    domu₃₂⊑v , w⊑codu₃₂    
sub-inv {u₁} {u₂} (⊑-trans{v = u} u₁⊑u u⊑u₂) {v} {w} v↦w∈u₁
    with sub-inv u₁⊑u v↦w∈u₁
... |  u′ ,  fu′ ,  u′⊆u ,  domu′⊑v , w⊑codu′    
    with sub-inv-trans {u′} fu′ u′⊆u (sub-inv u⊑u₂)
... |  u₃ ,  fu₃ ,  u₃⊆u₂ ,  domu₃⊑domu′ , codu′⊑codu₃     =
       u₃ ,  fu₃ ,  u₃⊆u₂ ,  ⊑-trans domu₃⊑domu′ domu′⊑v ,
                                    ⊑-trans w⊑codu′ codu′⊑codu₃    
sub-inv {u₁₁  u₁₂} {u₂₁  u₂₂} (⊑-fun lt1 lt2) refl =
     u₂₁  u₂₂ ,   {w}  fun) ,   {C} z  z) ,  lt1 , lt2    
sub-inv {u₂₁  (u₂₂  u₂₃)} {u₂₁  u₂₂  u₂₁  u₂₃} ⊑-dist
    {.u₂₁} {.(u₂₂  u₂₃)} refl =
     u₂₁  u₂₂  u₂₁  u₂₃ ,  f ,  g ,  ⊑-conj-L ⊑-refl ⊑-refl , ⊑-refl    
  where f : all-funs (u₂₁  u₂₂  u₂₁  u₂₃)
        f (inj₁ x) = fun x
        f (inj₂ y) = fun y
        g : (u₂₁  u₂₂  u₂₁  u₂₃)  (u₂₁  u₂₂  u₂₁  u₂₃)
        g (inj₁ x) = inj₁ x
        g (inj₂ y) = inj₂ y

vw 为任意值。

  • 情况 ⊑-bot:若 u₁ ≡ ⊥,我们有 v ↦ w ∈ ⊥,但这是不可能的。

  • 情况 ⊑-conj-L

      u₁₁ ⊑ u₂   u₁₂ ⊑ u₂
      -------------------
      u₁₁ ⊔ u₁₂ ⊑ u₂

给定 v ↦ w ∈ u₁₁ ⊔ u₁₂,那么有两种子情况需要考虑:

  • 子情况 v ↦ w ∈ u₁₁:我们通过归纳假设 u₁₁ ⊑ u₂ 得出结论。

  • 子情况 v ↦ w ∈ u₁₂:我们通过归纳假设 u₁₂ ⊑ u₂ 得出结论。

  • 情况 ⊑-conj-R1

      u₁ ⊑ u₂₁
      --------------
      u₁ ⊑ u₂₁ ⊔ u₂₂

给定 v ↦ w ∈ u₁,归纳假设 u₁ ⊑ u₂₁ 给出了对某个 u₃₁v ↦ wu₂₁ 分解为 u₃₁。为了证明 v ↦ w 也将 u₂₁ ⊔ u₂₂ 分解为 u₃₁, 我们只需证明 u₃₁ ⊆ u₂₁ ⊔ u₂₂,而这一点可直接从 u₃₁ ⊆ u₂₁ 得出。

  • 情况 ⊑-conj-R2:此情况的论证过程与情况 ⊑-conj-R1 类似。

  • 情况 ⊑-trans

      u₁ ⊑ u   u ⊑ u₂
      ---------------
          u₁ ⊑ u₂

根据归纳假设 u₁ ⊑ u,我们可以证明对于某个值 u′v ↦ wu 分解为 u′, 于是我们有 all-funs u′u′ ⊆ u。跟举归纳假设 u ⊑ u₂,我们可以证明对于任意 v′ ↦ w′ ∈ uv′ ↦ w′ 分解了 u₂。现在应用引理 sub-inv-trans, 它会给出 u₃ 使得 (⨆dom u′) ↦ (⨆cod u′)u₂ 分解为 u₃。 我们证明了 v ↦ w 也将 u₂ 分解为 u₃。 从 ⨆dom u₃ ⊑ ⨆dom u′⨆dom u′ ⊑ v,我们可得出 ⨆dom u₃ ⊑ v。 从 w ⊑ ⨆cod u′⨆cod u′ ⊑ ⨆cod u₃,我们有 w ⊑ ⨆cod u₃,于是此情况就覆盖完整了。

  • 情况 ⊑-fun

      u₂₁ ⊑ u₁₁  u₁₂ ⊑ u₂₂
      ---------------------
      u₁₁ ↦ u₁₂ ⊑ u₂₁ ↦ u₂₂

给定 v ↦ w ∈ u₁₁ ↦ u₁₂,我们有 v ≡ u₁₁w ≡ u₁₂。 我们证明了 u₁₁ ↦ u₁₂u₂₁ ↦ u₂₂ 分解为其自身。 我们还需证明 ⨆dom (u₂₁ ↦ u₂₂) ⊑ u₁₁u₁₂ ⊑ ⨆cod (u₂₁ ↦ u₂₂), 然而这等价于前提 u₂₁ ⊑ u₁₁u₁₂ ⊑ u₂₂

  • 情况 ⊑-dist

      ---------------------------------------------
      u₂₁ ↦ (u₂₂ ⊔ u₂₃) ⊑ (u₂₁ ↦ u₂₂) ⊔ (u₂₁ ↦ u₂₃)

给定 v ↦ w ∈ u₂₁ ↦ (u₂₂ ⊔ u₂₃),我们有 v ≡ u₂₁w ≡ u₂₂ ⊔ u₂₃。 我们证明了 u₂₁ ↦ (u₂₂ ⊔ u₂₃)(u₂₁ ↦ u₂₂) ⊔ (u₂₁ ↦ u₂₃) 分解为其自身。 我们有 u₂₁ ⊔ u₂₁ ⊑ u₂₁,以及 u₂₂ ⊔ u₂₃ ⊑ u₂₂ ⊔ u₂₃,于是此证明就覆盖完整了。

我们用 sub-inv 引理的两个推论作为本节的结尾。首先,我们来证明以下性质, 方便在后面的证明中使用。我们将前提特化为 v ↦ w ⊑ u₁,并将结论修改为对于每个 v′ ↦ w′ ∈ u₂,我们有 v′ ⊑ v

sub-inv-fun : ∀{v w u₁ : Value}
     (v  w)  u₁
      -----------------------------------------------------
     Σ[ u₂  Value ] all-funs u₂ × u₂  u₁
        × (∀{v′ w′}  (v′  w′)  u₂  v′  v) × w  ⨆cod u₂
sub-inv-fun{v}{w}{u₁} abc
    with sub-inv abc {v}{w} refl
... |  u₂ ,  f ,  u₂⊆u₁ ,  db , cc     =
       u₂ ,  f ,  u₂⊆u₁ ,  G , cc    
   where G : ∀{D E}  (D  E)  u₂  D  v
         G{D}{E} m = ⊑-trans (⊆→⊑ (↦∈→⊆⨆dom f m)) db

第二个推论是反转规则,即我们期望左侧函数小于右侧。

↦⊑↦-inv : ∀{v w v′ w′}
         v  w  v′  w′
          -----------------
         v′  v × w  w′
↦⊑↦-inv{v}{w}{v′}{w′} lt
    with sub-inv-fun lt
... |  Γ ,  f ,  Γ⊆v34 ,  lt1 , lt2    
    with all-funs∈ f
... |  u ,  u′ , u↦u′∈Γ  
    with Γ⊆v34 u↦u′∈Γ
... | refl =
  let ⨆codΓ⊆w′ = ⊆↦→⨆cod⊆ Γ⊆v34 in
   lt1 u↦u′∈Γ , ⊑-trans lt2 (⊆→⊑ ⨆codΓ⊆w′) 

注记

本章中展示的操作语义是过滤器模型(filter model)H. Barendregt, Coppo, and Dezani-Ciancaglini (1983))的一个例子。 过滤器模型使用带有交集类型的类型系统来精确刻画运行时行为(Coppo, Dezani-Ciancaglini, and Salle’ (1979))。 我们在本章中使用的记法并不是类型系统和交集类型的记法,但 Value 数据类型与类型同构( 对应 对应 对应 ), 关系是子类型 <: 的逆关系,并且求值关系 ρ ⊢ M ↓ v 与类型系统同构。 将 ρ 写成 Γ,将 v 写成 A,将 替换为 :,就得到了一个类型判断 Γ ⊢ M : A。通过改变子类型的定义和使用类型原子的不同选择, 交集类型系统为许多不同的无类型 λ-演算提供语义,从完整的 beta-规约到惰性演算和按值调用演算(Alessi, Barbanera, and Dezani-Ciancaglini (2006))(Ronchi Della Rocca and Paolini (2004))。 本章中的指称语义对应于 BCD 系统(H. Barendregt, Coppo, and Dezani-Ciancaglini (1983))。 Lambda Calculus with Types 一书中的第三部分描述了一个交集类型系统的框架, 它可以实现与本章中类似的结果,但适用于整个交集类型系统族(H. Barendregt, Dekkers, and Statman (2013)

使用有限表来表示函数,以及放宽表的查找以实现自我应用这两个想法首次出现在 Plotkin (1972) 的技术报告中,后来在《理论计算机科学》(Plotkin (1993)) 的一篇文章中进行了描述。在该项工作中,Value 的归纳定义与我们使用的有点不同:

Value = C + ℘f(Value) × ℘f(Value)

其中 C 是一组常量,℘f 表示有限幂集。℘f(Value) × ℘f(Value) 中的序对表示输入-输出映射,和本章中的一样。有限幂集用于使函数表能够出现在输入和输出中。 这些差异相当于改变了 Value 定义中递归出现的位置。Plotkin 的模型是无类型 λ-演算的图模型(graph model)示例(H. P. Barendregt (1984))。 在图模型中,语义被表示为从程序和环境到(可能是无限的)值的集合的函数。 本章中的语义被定义为关系,不过以集合为值的函数与关系同构。 实际上,我们将在下一章中将语义表示为函数,并证明它等价于关系的版本。

Scott (1976) 的 ℘(ω) 模型和 Engeler (1981) 的 B(A) 模型是图模型的另外两个例子。 二者都试用了以下 Value 的归纳定义。

Value = C + ℘f(Value) × Value

在输出中使用 Value 而非 ℘f(Value) 相对 Plotkin 的模型来说并不会限制表达能力, 因为使用了值的集合的语义和集合 (V, V′) 的序对可以表示为一个序对的集合 { (V, v′) | v′ ∈ V′ }。在 Scott 的 ℘(ω) 中,上面的值通过一种哥德尔编码做了 到自然数的双向映射。

Unicode

本章使用了以下 Unicode:

⊥  U+22A5  UP TACK (\bot)
↦  U+21A6  RIGHTWARDS ARROW FROM BAR (\mapsto)
⊔  U+2294  SQUARE CUP (\lub)
⊑  U+2291  SQUARE IMAGE OF OR EQUAL TO (\sqsubseteq)
⨆ U+2A06  N-ARY SQUARE UNION OPERATOR (\Lub)
⊢  U+22A2  RIGHT TACK (\|- or \vdash)
↓  U+2193  DOWNWARDS ARROW (\d)
ᶜ  U+1D9C  MODIFIER LETTER SMALL C (\^c)
ℰ  U+2130  SCRIPT CAPITAL E (\McE)
≃  U+2243  ASYMPTOTICALLY EQUAL TO (\~- or \simeq)
∈  U+2208  ELEMENT OF (\in)
⊆  U+2286  SUBSET OF OR EQUAL TO (\sub= or \subseteq)

参考来源

Compositional: The denotational semantics is compositional

module plfa.part3.Compositional where

Introduction

In this chapter we prove that the denotational semantics is compositional, which means we fill in the ellipses in the following equations.

ℰ (` x) ≃ ...
ℰ (ƛ M) ≃ ... ℰ M ...
ℰ (M · N) ≃ ... ℰ M ... ℰ N ...

Such equations would imply that the denotational semantics could be instead defined as a recursive function. Indeed, we end this chapter with such a definition and prove that it is equivalent to ℰ.

Imports

open import Data.Product using (_×_; Σ; Σ-syntax; ; ∃-syntax; proj₁; proj₂)
  renaming (_,_ to ⟨_,_⟩)
open import Data.Sum using (_⊎_; inj₁; inj₂)
open import Data.Unit using (; tt)
open import plfa.part2.Untyped
  using (Context; _,_; ; _∋_; _⊢_; `_; ƛ_; _·_)
open import plfa.part3.Denotational
  using (Value; _↦_; _`,_; _⊔_; ; _⊑_; _⊢_↓_;
         ⊑-bot; ⊑-fun; ⊑-conj-L; ⊑-conj-R1; ⊑-conj-R2;
         ⊑-dist; ⊑-refl; ⊑-trans; ⊔↦⊔-dist;
         var; ↦-intro; ↦-elim; ⊔-intro; ⊥-intro; sub;
         up-env; ; _≃_; ≃-sym; Denotation; Env)
open plfa.part3.Denotational.≃-Reasoning

Equation for lambda abstraction

Regarding the first equation

ℰ (ƛ M) ≃ ... ℰ M ...

we need to define a function that maps a Denotation (Γ , ★) to a Denotation Γ. This function, let us name it , should mimic the non-recursive part of the semantics when applied to a lambda term. In particular, we need to consider the rules ↦-intro, ⊥-intro, and ⊔-intro. So has three parameters, the denotation D of the subterm M, an environment γ, and a value v. If we define by recursion on the value v, then it matches up nicely with the three rules ↦-intro, ⊥-intro, and ⊔-intro.

 : ∀{Γ}  Denotation (Γ , )  Denotation Γ
 D γ (v  w) = D (γ `, v) w
 D γ  = 
 D γ (u  v) = ( D γ u) × ( D γ v)

If one squints hard enough, the function starts to look like the curry operation familiar to functional programmers. It turns a function that expects a tuple of length n + 1 (the environment Γ , ★) into a function that expects a tuple of length n and returns a function of one parameter.

Using this , we hope to prove that

ℰ (ƛ N) ≃ ℱ (ℰ N)

The function is preserved when going from a larger value v to a smaller value u. The proof is a straightforward induction on the derivation of u ⊑ v, using the up-env lemma in the case for the ⊑-fun rule.

sub-ℱ : ∀{Γ}{N : Γ ,   }{γ v u}
    ( N) γ v
   u  v
    ------------
    ( N) γ u
sub-ℱ d ⊑-bot = tt
sub-ℱ d (⊑-fun lt lt′) = sub (up-env d lt) lt′
sub-ℱ d (⊑-conj-L lt lt₁) =  sub-ℱ d lt , sub-ℱ d lt₁ 
sub-ℱ d (⊑-conj-R1 lt) = sub-ℱ (proj₁ d) lt
sub-ℱ d (⊑-conj-R2 lt) = sub-ℱ (proj₂ d) lt
sub-ℱ {v = v₁  v₂  v₁  v₃} {v₁  (v₂  v₃)}  N2 , N3  ⊑-dist =
   ⊔-intro N2 N3
sub-ℱ d (⊑-trans x₁ x₂) = sub-ℱ (sub-ℱ d x₂) x₁

With this subsumption property in hand, we can prove the forward direction of the semantic equation for lambda. The proof is by induction on the semantics, using sub-ℱ in the case for the sub rule.

ℰƛ→ℱℰ : ∀{Γ}{γ : Env Γ}{N : Γ ,   }{v : Value}
    (ƛ N) γ v
    ------------
    ( N) γ v
ℰƛ→ℱℰ (↦-intro d) = d
ℰƛ→ℱℰ ⊥-intro = tt
ℰƛ→ℱℰ (⊔-intro d₁ d₂) =  ℰƛ→ℱℰ d₁ , ℰƛ→ℱℰ d₂ 
ℰƛ→ℱℰ (sub d lt) = sub-ℱ (ℰƛ→ℱℰ d) lt

The “inversion lemma” for lambda abstraction is a special case of the above. The inversion lemma is useful in proving that denotations are preserved by reduction.

lambda-inversion : ∀{Γ}{γ : Env Γ}{N : Γ ,   }{v₁ v₂ : Value}
   γ  ƛ N  v₁  v₂
    -----------------
   (γ `, v₁)  N  v₂
lambda-inversion{v₁ = v₁}{v₂ = v₂} d = ℰƛ→ℱℰ{v = v₁  v₂} d

The backward direction of the semantic equation for lambda is even easier to prove than the forward direction. We proceed by induction on the value v.

ℱℰ→ℰƛ : ∀{Γ}{γ : Env Γ}{N : Γ ,   }{v : Value}
    ( N) γ v
    ------------
    (ƛ N) γ v
ℱℰ→ℰƛ {v = } d = ⊥-intro
ℱℰ→ℰƛ {v = v₁  v₂} d = ↦-intro d
ℱℰ→ℰƛ {v = v₁  v₂}  d1 , d2  = ⊔-intro (ℱℰ→ℰƛ d1) (ℱℰ→ℰƛ d2)

So indeed, the denotational semantics is compositional with respect to lambda abstraction, as witnessed by the function .

lam-equiv : ∀{Γ}{N : Γ ,   }
    (ƛ N)   ( N)
lam-equiv γ v =  ℰƛ→ℱℰ , ℱℰ→ℰƛ 

Equation for function application

Next we fill in the ellipses for the equation concerning function application.

ℰ (M · N) ≃ ... ℰ M ... ℰ N ...

For this we need to define a function that takes two denotations, both in context Γ, and produces another one in context Γ. This function, let us name it , needs to mimic the non-recursive aspects of the semantics of an application L · M. We cannot proceed as easily as for and define the function by recursion on value v because, for example, the rule ↦-elim applies to any value. Instead we shall define in a way that directly deals with the ↦-elim and ⊥-intro rules but ignores ⊔-intro. This makes the forward direction of the proof more difficult, and the case for ⊔-intro demonstrates why the ⊑-dist rule is important.

So we define the application of D₁ to D₂, written D₁ ● D₂, to include any value w equivalent to , for the ⊥-intro rule, and to include any value w that is the output of an entry v ↦ w in D₁, provided the input v is in D₂, for the ↦-elim rule.

infixl 7 _●_

_●_ : ∀{Γ}  Denotation Γ  Denotation Γ  Denotation Γ
(D₁  D₂) γ w = w    Σ[ v  Value ]( D₁ γ (v  w) × D₂ γ v )

If one squints hard enough, the _●_ operator starts to look like the apply operation familiar to functional programmers. It takes two parameters and applies the first to the second.

Next we consider the inversion lemma for application, which is also the forward direction of the semantic equation for application. We describe the proof below.

ℰ·→●ℰ : ∀{Γ}{γ : Env Γ}{L M : Γ  }{v : Value}
    (L · M) γ v
    ----------------
   ( L   M) γ v
ℰ·→●ℰ (↦-elim{v = v′} d₁ d₂) = inj₂  v′ ,  d₁ , d₂  
ℰ·→●ℰ {v = } ⊥-intro = inj₁ ⊑-bot
ℰ·→●ℰ {Γ}{γ}{L}{M}{v} (⊔-intro{v = v₁}{w = v₂} d₁ d₂)
    with ℰ·→●ℰ d₁ | ℰ·→●ℰ d₂
... | inj₁ lt1 | inj₁ lt2 = inj₁ (⊑-conj-L lt1 lt2)
... | inj₁ lt1 | inj₂  v₁′ ,  L↓v12 , M↓v3   =
      inj₂  v₁′ ,  sub L↓v12 lt , M↓v3  
      where lt : v₁′  (v₁  v₂)  v₁′  v₂
            lt = (⊑-fun ⊑-refl (⊑-conj-L (⊑-trans lt1 ⊑-bot) ⊑-refl))
... | inj₂  v₁′ ,  L↓v12 , M↓v3   | inj₁ lt2 =
      inj₂  v₁′ ,  sub L↓v12 lt , M↓v3  
      where lt : v₁′  (v₁  v₂)  v₁′  v₁
            lt = (⊑-fun ⊑-refl (⊑-conj-L ⊑-refl (⊑-trans lt2 ⊑-bot)))
... | inj₂  v₁′ ,  L↓v12 , M↓v3   | inj₂  v₁′′ ,  L↓v12′ , M↓v3′   =
      let L↓⊔ = ⊔-intro L↓v12 L↓v12′ in
      let M↓⊔ = ⊔-intro M↓v3 M↓v3′ in
      inj₂  v₁′  v₁′′ ,  sub L↓⊔ ⊔↦⊔-dist , M↓⊔  
ℰ·→●ℰ {Γ}{γ}{L}{M}{v} (sub d lt)
    with ℰ·→●ℰ d
... | inj₁ lt2 = inj₁ (⊑-trans lt lt2)
... | inj₂  v₁ ,  L↓v12 , M↓v3   =
      inj₂  v₁ ,  sub L↓v12 (⊑-fun ⊑-refl lt) , M↓v3  

We proceed by induction on the semantics.

  • In case ↦-elim we have γ ⊢ L ↓ (v′ ↦ v) and γ ⊢ M ↓ v′, which is all we need to show (ℰ L ● ℰ M) γ v.

  • In case ⊥-intro we have v = ⊥. We conclude that v ⊑ ⊥.

  • In case ⊔-intro we have ℰ (L · M) γ v₁ and ℰ (L · M) γ v₂ and need to show (ℰ L ● ℰ M) γ (v₁ ⊔ v₂). By the induction hypothesis, we have (ℰ L ● ℰ M) γ v₁ and (ℰ L ● ℰ M) γ v₂. We have four subcases to consider.

    • Suppose v₁ ⊑ ⊥ and v₂ ⊑ ⊥. Then v₁ ⊔ v₂ ⊑ ⊥.

    • Suppose v₁ ⊑ ⊥, γ ⊢ L ↓ v₁′ ↦ v₂, and γ ⊢ M ↓ v₁′. We have γ ⊢ L ↓ v₁′ ↦ (v₁ ⊔ v₂) by rule sub because v₁′ ↦ (v₁ ⊔ v₂) ⊑ v₁′ ↦ v₂.

    • Suppose γ ⊢ L ↓ v₁′ ↦ v₁, γ ⊢ M ↓ v₁′, and v₂ ⊑ ⊥. We have γ ⊢ L ↓ v₁′ ↦ (v₁ ⊔ v₂) by rule sub because v₁′ ↦ (v₁ ⊔ v₂) ⊑ v₁′ ↦ v₁.

    • Suppose γ ⊢ L ↓ v₁′′ ↦ v₁, γ ⊢ M ↓ v₁′′, γ ⊢ L ↓ v₁′ ↦ v₂, and γ ⊢ M ↓ v₁′. This case is the most interesting. By two uses of the rule ⊔-intro we have γ ⊢ L ↓ (v₁′ ↦ v₂) ⊔ (v₁′′ ↦ v₁) and γ ⊢ M ↓ (v₁′ ⊔ v₁′′). But this does not yet match what we need for ℰ L ● ℰ M because the result of L must be an whose input entry is v₁′ ⊔ v₁′′. So we use the sub rule to obtain γ ⊢ L ↓ (v₁′ ⊔ v₁′′) ↦ (v₁ ⊔ v₂), using the ⊔↦⊔-dist lemma (thanks to the ⊑-dist rule) to show that

        (v₁′ ⊔ v₁′′) ↦ (v₁ ⊔ v₂) ⊑ (v₁′ ↦ v₂) ⊔ (v₁′′ ↦ v₁)

      So we have proved what is needed for this case.

  • In case sub we have Γ ⊢ L · M ↓ v₁ and v ⊑ v₁. By the induction hypothesis, we have (ℰ L ● ℰ M) γ v₁. We have two subcases to consider.

    • Suppose v₁ ⊑ ⊥. We conclude that v ⊑ ⊥.
    • Suppose Γ ⊢ L ↓ v′ → v₁ and Γ ⊢ M ↓ v′. We conclude with Γ ⊢ L ↓ v′ → v by rule sub, because v′ → v ⊑ v′ → v₁.

The forward direction is proved by cases on the premise (ℰ L ● ℰ M) γ v. In case v ⊑ ⊥, we obtain Γ ⊢ L · M ↓ ⊥ by rule ⊥-intro. Otherwise, we conclude immediately by rule ↦-elim.

●ℰ→ℰ· : ∀{Γ}{γ : Env Γ}{L M : Γ  }{v}
   ( L   M) γ v
    ----------------
    (L · M) γ v
●ℰ→ℰ· {γ}{v} (inj₁ lt) = sub ⊥-intro lt
●ℰ→ℰ· {γ}{v} (inj₂  v₁ ,  d1 , d2  ) = ↦-elim d1 d2

So we have proved that the semantics is compositional with respect to function application, as witnessed by the function.

app-equiv : ∀{Γ}{L M : Γ  }
    (L · M)  ( L)  ( M)
app-equiv γ v =  ℰ·→●ℰ , ●ℰ→ℰ· 

We also need an inversion lemma for variables. If Γ ⊢ x ↓ v, then v ⊑ γ x. The proof is a straightforward induction on the semantics.

var-inv :  {Γ v x} {γ : Env Γ}
    (` x) γ v
    -------------------
   v  γ x
var-inv (var) = ⊑-refl
var-inv (⊔-intro d₁ d₂) = ⊑-conj-L (var-inv d₁) (var-inv d₂)
var-inv (sub d lt) = ⊑-trans lt (var-inv d)
var-inv ⊥-intro = ⊑-bot

To round-out the semantic equations, we establish the following one for variables.

var-equiv : ∀{Γ}{x : Γ  }   (` x)   γ v  v  γ x)
var-equiv γ v =  var-inv ,  lt  sub var lt) 

Congruence

The main work of this chapter is complete: we have established semantic equations that show how the denotational semantics is compositional. In this section and the next we make use of these equations to prove some corollaries: that denotational equality is a congruence and to prove the compositionality property, which states that surrounding two denotationally-equal terms in the same context produces two programs that are denotationally equal.

We begin by showing that denotational equality is a congruence with respect to lambda abstraction: that ℰ N ≃ ℰ N′ implies ℰ (ƛ N) ≃ ℰ (ƛ N′). We shall use the lam-equiv equation to reduce this question to whether is a congruence.

ℱ-cong : ∀{Γ}{D D′ : Denotation (Γ , )}
   D  D′
    -----------
    D   D′
ℱ-cong{Γ} D≃D′ γ v =
    x  ℱ≃{γ}{v} x D≃D′) ,  x  ℱ≃{γ}{v} x (≃-sym D≃D′)) 
  where
  ℱ≃ : ∀{γ : Env Γ}{v}{D D′ : Denotation (Γ , )}
      D γ v    D  D′   D′ γ v
  ℱ≃ {v = } fd dd′ = tt
  ℱ≃ {γ}{v  w} fd dd′ = proj₁ (dd′ (γ `, v) w) fd
  ℱ≃ {γ}{u  w} fd dd′ =  ℱ≃{γ}{u} (proj₁ fd) dd′ , ℱ≃{γ}{w} (proj₂ fd) dd′ 

The proof of ℱ-cong uses the lemma ℱ≃ to handle both directions of the if-and-only-if. That lemma is proved by a straightforward induction on the value v.

We now prove that lambda abstraction is a congruence by direct equational reasoning.

lam-cong : ∀{Γ}{N N′ : Γ ,   }
    N   N′
    -----------------
    (ƛ N)   (ƛ N′)
lam-cong {Γ}{N}{N′} N≃N′ =
  start
     (ƛ N)
  ≃⟨ lam-equiv 
     ( N)
  ≃⟨ ℱ-cong N≃N′ 
     ( N′)
  ≃⟨ ≃-sym lam-equiv 
     (ƛ N′)
  

Next we prove that denotational equality is a congruence for application: that ℰ L ≃ ℰ L′ and ℰ M ≃ ℰ M′ imply ℰ (L · M) ≃ ℰ (L′ · M′). The app-equiv equation reduces this to the question of whether the operator is a congruence.

●-cong : ∀{Γ}{D₁ D₁′ D₂ D₂′ : Denotation Γ}
   D₁  D₁′  D₂  D₂′
   (D₁  D₂)  (D₁′  D₂′)
●-cong {Γ} d1 d2 γ v =   x  ●≃ x d1 d2) ,
                          x  ●≃ x (≃-sym d1) (≃-sym d2)) 
  where
  ●≃ : ∀{γ : Env Γ}{v}{D₁ D₁′ D₂ D₂′ : Denotation Γ}
     (D₁  D₂) γ v    D₁  D₁′    D₂  D₂′
     (D₁′  D₂′) γ v
  ●≃ (inj₁ v⊑⊥) eq₁ eq₂ = inj₁ v⊑⊥
  ●≃ {γ} {w} (inj₂  v ,  Dv↦w , Dv  ) eq₁ eq₂ =
    inj₂  v ,  proj₁ (eq₁ γ (v  w)) Dv↦w , proj₁ (eq₂ γ v) Dv  

Again, both directions of the if-and-only-if are proved via a lemma. This time the lemma is proved by cases on (D₁ ● D₂) γ v.

With the congruence of , we can prove that application is a congruence by direct equational reasoning.

app-cong : ∀{Γ}{L L′ M M′ : Γ  }
    L   L′
    M   M′
    -------------------------
    (L · M)   (L′ · M′)
app-cong {Γ}{L}{L′}{M}{M′} L≅L′ M≅M′ =
  start
     (L · M)
  ≃⟨ app-equiv 
     L   M
  ≃⟨ ●-cong L≅L′ M≅M′ 
     L′   M′
  ≃⟨ ≃-sym app-equiv 
     (L′ · M′)
  

Compositionality

The compositionality property states that surrounding two terms that are denotationally equal in the same context produces two programs that are denotationally equal. To make this precise, we define what we mean by “context” and “surround”.

A context is a program with one hole in it. The following data definition Ctx makes this idea explicit. We index the Ctx data type with two contexts for variables: one for the hole and one for terms that result from filling the hole.

data Ctx : Context  Context  Set where
  ctx-hole : ∀{Γ}  Ctx Γ Γ
  ctx-lam :  ∀{Γ Δ}  Ctx (Γ , ) (Δ , )  Ctx (Γ , ) Δ
  ctx-app-L : ∀{Γ Δ}  Ctx Γ Δ  Δ    Ctx Γ Δ
  ctx-app-R : ∀{Γ Δ}  Δ    Ctx Γ Δ  Ctx Γ Δ
  • The constructor ctx-hole represents the hole, and in this case the variable context for the hole is the same as the variable context for the term that results from filling the hole.

  • The constructor ctx-lam takes a Ctx and produces a larger one that adds a lambda abstraction at the top. The variable context of the hole stays the same, whereas we remove one variable from the context of the resulting term because it is bound by this lambda abstraction.

  • There are two constructions for application, ctx-app-L and ctx-app-R. The ctx-app-L is for when the hole is inside the left-hand term (the operator) and the later is when the hole is inside the right-hand term (the operand).

The action of surrounding a term with a context is defined by the following plug function. It is defined by recursion on the context.

plug : ∀{Γ}{Δ}  Ctx Γ Δ  Γ    Δ  
plug ctx-hole M = M
plug (ctx-lam C) N = ƛ plug C N
plug (ctx-app-L C N) L = (plug C L) · N
plug (ctx-app-R L C) M = L · (plug C M)

We are ready to state and prove the compositionality principle. Given two terms M and N that are denotationally equal, plugging them both into an arbitrary context C produces two programs that are denotationally equal.

compositionality : ∀{Γ Δ}{C : Ctx Γ Δ} {M N : Γ  }
    M   N
    ---------------------------
    (plug C M)   (plug C N)
compositionality {C = ctx-hole} M≃N =
  M≃N
compositionality {C = ctx-lam C′} M≃N =
  lam-cong (compositionality {C = C′} M≃N)
compositionality {C = ctx-app-L C′ L} M≃N =
  app-cong (compositionality {C = C′} M≃N) λ γ v    x  x) ,  x  x) 
compositionality {C = ctx-app-R L C′} M≃N =
  app-cong  γ v    x  x) ,  x  x) ) (compositionality {C = C′} M≃N)

The proof is a straightforward induction on the context C, using the congruence properties lam-cong and app-cong that we established above.

The denotational semantics defined as a function

Having established the three equations var-equiv, lam-equiv, and app-equiv, one should be able to define the denotational semantics as a recursive function over the input term M. Indeed, we define the following function ⟦ M ⟧ that maps terms to denotations, using the auxiliary curry and apply functions in the cases for lambda and application, respectively.

⟦_⟧ : ∀{Γ}  (M : Γ  )  Denotation Γ
 ` x  γ v = v  γ x
 ƛ N  =   N 
 L · M  =  L    M 

The proof that ℰ M is denotationally equal to ⟦ M ⟧ is a straightforward induction, using the three equations var-equiv, lam-equiv, and app-equiv together with the congruence lemmas for and .

ℰ≃⟦⟧ :  {Γ} {M : Γ  }   M   M 
ℰ≃⟦⟧ {Γ} {` x} = var-equiv
ℰ≃⟦⟧ {Γ} {ƛ N} =
  let ih = ℰ≃⟦⟧ {M = N} in
     (ƛ N)
  ≃⟨ lam-equiv 
     ( N)
  ≃⟨ ℱ-cong (ℰ≃⟦⟧ {M = N}) 
      N 
  ≃⟨⟩
     ƛ N 
  
ℰ≃⟦⟧ {Γ} {L · M} =
    (L · M)
  ≃⟨ app-equiv 
    L   M
  ≃⟨ ●-cong (ℰ≃⟦⟧ {M = L}) (ℰ≃⟦⟧ {M = M}) 
    L    M 
  ≃⟨⟩
     L · M 
  

Unicode

This chapter uses the following unicode:

ℱ  U+2131  SCRIPT CAPITAL F (\McF)
●  U+2131  BLACK CIRCLE (\cib)

Soundness: Soundness of reduction with respect to denotational semantics

module plfa.part3.Soundness where

Introduction

In this chapter we prove that the reduction semantics is sound with respect to the denotational semantics, i.e., for any term L

L —↠ ƛ N  implies  ℰ L ≃ ℰ (ƛ N)

The proof is by induction on the reduction sequence, so the main lemma concerns a single reduction step. We prove that if any term M steps to a term N, then M and N are denotationally equal. We shall prove each direction of this if-and-only-if separately. One direction will look just like a type preservation proof. The other direction is like proving type preservation for reduction going in reverse. Recall that type preservation is sometimes called subject reduction. Preservation in reverse is a well-known property and is called subject expansion. It is also well-known that subject expansion is false for most typed lambda calculi!

Imports

open import Relation.Binary.PropositionalEquality
  using (_≡_; _≢_; refl; sym; cong; cong₂; cong-app)
open import Data.Product using (_×_; Σ; Σ-syntax; ; ∃-syntax; proj₁; proj₂)
  renaming (_,_ to ⟨_,_⟩)
open import Agda.Primitive using (lzero)
open import Relation.Nullary using (¬_)
open import Relation.Nullary.Negation using (contradiction)
open import Data.Empty using (⊥-elim)
open import Relation.Nullary using (Dec; yes; no)
open import Function using (_∘_)
open import plfa.part2.Untyped
     using (Context; _,_; _∋_; _⊢_; ; Z; S_; `_; ƛ_; _·_;
            subst; _[_]; subst-zero; ext; rename; exts;
            _—→_; ξ₁; ξ₂; β; ζ; _—↠_; _—→⟨_⟩_; _∎)
open import plfa.part2.Substitution using (Rename; Subst; ids)
open import plfa.part3.Denotational
     using (Value; ; Env; _⊢_↓_; _`,_; _⊑_; _`⊑_; `⊥; _`⊔_; init; last; init-last;
            ⊑-refl; ⊑-trans; `⊑-refl; ⊑-env; ⊑-env-conj-R1; ⊑-env-conj-R2; up-env;
            var; ↦-elim; ↦-intro; ⊥-intro; ⊔-intro; sub;
            rename-pres; ; _≃_; ≃-trans)
open import plfa.part3.Compositional using (lambda-inversion; var-inv)

Forward reduction preserves denotations

The proof of preservation in this section mixes techniques from previous chapters. Like the proof of preservation for the STLC, we are preserving a relation defined separately from the syntax, in contrast to the intrinsically-typed terms. On the other hand, we are using de Bruijn indices for variables.

The outline of the proof remains the same in that we must prove lemmas concerning all of the auxiliary functions used in the reduction relation: substitution, renaming, and extension.

Simultaneous substitution preserves denotations

Our next goal is to prove that simultaneous substitution preserves meaning. That is, if M results in v in environment γ, then applying a substitution σ to M gives us a program that also results in v, but in an environment δ in which, for every variable x, σ x results in the same value as the one for x in the original environment γ. We write δ `⊢ σ ↓ γ for this condition.

infix 3 _`⊢_↓_
_`⊢_↓_ : ∀{Δ Γ}  Env Δ  Subst Γ Δ  Env Γ  Set
_`⊢_↓_ {Δ}{Γ} δ σ γ = (∀ (x : Γ  )  δ  σ x  γ x)

As usual, to prepare for lambda abstraction, we prove an extension lemma. It says that applying the exts function to a substitution produces a new substitution that maps variables to terms that when evaluated in δ , v produce the values in γ , v.

subst-ext :  {Γ Δ v} {γ : Env Γ} {δ : Env Δ}
   (σ : Subst Γ Δ)
   δ `⊢ σ  γ
   --------------------------
   δ `, v `⊢ exts σ  γ `, v
subst-ext σ d Z = var
subst-ext σ d (S x′) = rename-pres S_  _  ⊑-refl) (d x′)

The proof is by cases on the de Bruijn index x.

  • If it is Z, then we need to show that δ , v ⊢ # 0 ↓ v, which we have by rule var.

  • If it is S x′,then we need to show that δ , v ⊢ rename S_ (σ x′) ↓ γ x′, which we obtain by the rename-pres lemma.

With the extension lemma in hand, the proof that simultaneous substitution preserves meaning is straightforward. Let’s dive in!

subst-pres :  {Γ Δ v} {γ : Env Γ} {δ : Env Δ} {M : Γ  }
   (σ : Subst Γ Δ)
   δ `⊢ σ  γ
   γ  M  v
    ------------------
   δ  subst σ M  v
subst-pres σ s (var {x = x}) = s x
subst-pres σ s (↦-elim d₁ d₂) =
  ↦-elim (subst-pres σ s d₁) (subst-pres σ s d₂)
subst-pres σ s (↦-intro d) =
  ↦-intro (subst-pres  {A}  exts σ) (subst-ext σ s) d)
subst-pres σ s ⊥-intro = ⊥-intro
subst-pres σ s (⊔-intro d₁ d₂) =
  ⊔-intro (subst-pres σ s d₁) (subst-pres σ s d₂)
subst-pres σ s (sub d lt) = sub (subst-pres σ s d) lt

The proof is by induction on the semantics of M. The two interesting cases are for variables and lambda abstractions.

  • For a variable x, we have that v = γ x and we need to show that δ ⊢ σ x ↓ v. From the premise applied to x, we have that δ ⊢ σ x ↓ γ x aka δ ⊢ σ x ↓ v.

  • For a lambda abstraction, we must extend the substitution for the induction hypothesis. We apply the subst-ext lemma to show that the extended substitution maps variables to terms that result in the appropriate values.

Single substitution preserves denotations

For β reduction, (ƛ N) · M —→ N [ M ], we need to show that the semantics is preserved when substituting M for de Bruijn index 0 in term N. By inversion on the rules ↦-elim and ↦-intro, we have that γ , v ⊢ M ↓ w and γ ⊢ N ↓ v. So we need to show that γ ⊢ M [ N ] ↓ w, or equivalently, that γ ⊢ subst (subst-zero N) M ↓ w.

substitution :  {Γ} {γ : Env Γ} {N M v w}
    γ `, v  N  w
    γ  M  v
     ---------------
    γ  N [ M ]  w
substitution{Γ}{γ}{N}{M}{v}{w} dn dm =
  subst-pres (subst-zero M) sub-z-ok dn
  where
  sub-z-ok : γ `⊢ subst-zero M  (γ `, v)
  sub-z-ok Z = dm
  sub-z-ok (S x) = var

This result is a corollary of the lemma for simultaneous substitution. To use the lemma, we just need to show that subst-zero M maps variables to terms that produces the same values as those in γ , v. Let y be an arbitrary variable (de Bruijn index).

  • If it is Z, then (subst-zero M) y = M and (γ , v) y = v. By the premise we conclude that γ ⊢ M ↓ v.

  • If it is S x, then (subst-zero M) (S x) = x and (γ , v) (S x) = γ x. So we conclude that γ ⊢ x ↓ γ x by rule var.

Reduction preserves denotations

With the substitution lemma in hand, it is straightforward to prove that reduction preserves denotations.

preserve :  {Γ} {γ : Env Γ} {M N v}
   γ  M  v
   M —→ N
    ----------
   γ  N  v
preserve (var) ()
preserve (↦-elim d₁ d₂) (ξ₁ r) = ↦-elim (preserve d₁ r) d₂
preserve (↦-elim d₁ d₂) (ξ₂ r) = ↦-elim d₁ (preserve d₂ r)
preserve (↦-elim d₁ d₂) β = substitution (lambda-inversion d₁) d₂
preserve (↦-intro d) (ζ r) = ↦-intro (preserve d r)
preserve ⊥-intro r = ⊥-intro
preserve (⊔-intro d d₁) r = ⊔-intro (preserve d r) (preserve d₁ r)
preserve (sub d lt) r = sub (preserve d r) lt

We proceed by induction on the semantics of M with case analysis on the reduction.

  • If M is a variable, then there is no such reduction.

  • If M is an application, then the reduction is either a congruence (ξ₁ or ξ₂) or β. For each congruence, we use the induction hypothesis. For β reduction we use the substitution lemma and the sub rule.

  • The rest of the cases are straightforward.

Reduction reflects denotations

This section proves that reduction reflects the denotation of a term. That is, if N results in v, and if M reduces to N, then M also results in v. While there are some broad similarities between this proof and the above proof of semantic preservation, we shall require a few more technical lemmas to obtain this result.

The main challenge is dealing with the substitution in β reduction:

(ƛ N) · M —→ N [ M ]

We have that γ ⊢ N [ M ] ↓ v and need to show that γ ⊢ (ƛ N) · M ↓ v. Now consider the derivation of γ ⊢ N [ M ] ↓ v. The term M may occur 0, 1, or many times inside N [ M ]. At each of those occurrences, M may result in a different value. But to build a derivation for (ƛ N) · M, we need a single value for M. If M occurred more than 1 time, then we can join all of the different values using . If M occurred 0 times, then we do not need any information about M and can therefore use for the value of M.

Renaming reflects meaning

Previously we showed that renaming variables preserves meaning. Now we prove the opposite, that it reflects meaning. That is, if δ ⊢ rename ρ M ↓ v, then γ ⊢ M ↓ v, where (δ ∘ ρ)⊑ γ`.

First, we need a variant of a lemma given earlier.
ext-`⊑ :  {Γ Δ v} {γ : Env Γ} {δ : Env Δ}
   (ρ : Rename Γ Δ)
   (δ  ρ) `⊑ γ
    ------------------------------
   ((δ `, v)  ext ρ) `⊑ (γ `, v)
ext-`⊑ ρ lt Z = ⊑-refl
ext-`⊑ ρ lt (S x) = lt x
The proof is then as follows.
rename-reflect :  {Γ Δ v} {γ : Env Γ} {δ : Env Δ} { M : Γ  }
   {ρ : Rename Γ Δ}
   (δ  ρ) `⊑ γ
   δ  rename ρ M  v
    ------------------------------------
   γ  M  v
rename-reflect {M = ` x} all-n d with var-inv d
... | lt =  sub var (⊑-trans lt (all-n x))
rename-reflect {M = ƛ N}{ρ = ρ} all-n (↦-intro d) =
   ↦-intro (rename-reflect (ext-`⊑ ρ all-n) d)
rename-reflect {M = ƛ N} all-n ⊥-intro = ⊥-intro
rename-reflect {M = ƛ N} all-n (⊔-intro d₁ d₂) =
   ⊔-intro (rename-reflect all-n d₁) (rename-reflect all-n d₂)
rename-reflect {M = ƛ N} all-n (sub d₁ lt) =
   sub (rename-reflect all-n d₁) lt
rename-reflect {M = L · M} all-n (↦-elim d₁ d₂) =
   ↦-elim (rename-reflect all-n d₁) (rename-reflect all-n d₂)
rename-reflect {M = L · M} all-n ⊥-intro = ⊥-intro
rename-reflect {M = L · M} all-n (⊔-intro d₁ d₂) =
   ⊔-intro (rename-reflect all-n d₁) (rename-reflect all-n d₂)
rename-reflect {M = L · M} all-n (sub d₁ lt) =
   sub (rename-reflect all-n d₁) lt

We cannot prove this lemma by induction on the derivation of δ ⊢ rename ρ M ↓ v, so instead we proceed by induction on M.

  • If it is a variable, we apply the inversion lemma to obtain that v ⊑ δ (ρ x). Instantiating the premise to x we have δ (ρ x) ⊑ γ x, so we conclude by the var rule.

  • If it is a lambda abstraction ƛ N, we have rename ρ (ƛ N) = ƛ (rename (ext ρ) N). We proceed by cases on δ ⊢ ƛ (rename (ext ρ) N) ↓ v.

    • Rule ↦-intro: To satisfy the premise of the induction hypothesis, we prove that the renaming can be extended to be a mapping from γ , v to δ , v.

    • Rule ⊥-intro: We simply apply ⊥-intro.

    • Rule ⊔-intro: We apply the induction hypotheses and ⊔-intro.

    • Rule sub: We apply the induction hypothesis and sub.

  • If it is an application L · M, we have rename ρ (L · M) = (rename ρ L) · (rename ρ M). We proceed by cases on δ ⊢ (rename ρ L) · (rename ρ M) ↓ v and all the cases are straightforward.

In the upcoming uses of rename-reflect, the renaming will always be the increment function. So we prove a corollary for that special case.

rename-inc-reflect :  {Γ v′ v} {γ : Env Γ} { M : Γ  }
   (γ `, v′)  rename S_ M  v
    ----------------------------
   γ  M  v
rename-inc-reflect d = rename-reflect `⊑-refl d

Substitution reflects denotations, the variable case

We are almost ready to begin proving that simultaneous substitution reflects denotations. That is, if γ ⊢ (subst σ M) ↓ v, then γ ⊢ σ k ↓ δ k and δ ⊢ M ↓ v for any k and some δ. We shall start with the case in which M is a variable x. So instead the premise is γ ⊢ σ x ↓ v and we need to show that δ ⊢ ` x ↓ v for some δ. The δ that we choose shall be the environment that maps x to v and every other variable to .

Next we define the environment that maps x to v and every other variable to , that is const-env x v. To tell variables apart, we define the following function for deciding equality of variables.

_var≟_ :  {Γ}  (x y : Γ  )  Dec (x  y)
Z var≟ Z  =  yes refl
Z var≟ (S _)  =  no λ()
(S _) var≟ Z  =  no λ()
(S x) var≟ (S y) with  x var≟ y
...                 |  yes refl =  yes refl
...                 |  no neq   =  no λ{refl  neq refl}

var≟-refl :  {Γ} (x : Γ  )  (x var≟ x)  yes refl
var≟-refl Z = refl
var≟-refl (S x) rewrite var≟-refl x = refl

Now we use var≟ to define const-env.

const-env : ∀{Γ}  (x : Γ  )  Value  Env Γ
const-env x v y with x var≟ y
...             | yes _       = v
...             | no _        = 

Of course, const-env x v maps x to value v

same-const-env : ∀{Γ} {x : Γ  } {v}  (const-env x v) x  v
same-const-env {x = x} rewrite var≟-refl x = refl

and const-env x v maps y to ⊥, so long asx ≢ y`.

diff-const-env : ∀{Γ} {x y : Γ  } {v}
   x  y
    -------------------
   const-env x v y  
diff-const-env {Γ} {x} {y} neq with x var≟ y
...  | yes eq  =  ⊥-elim (neq eq)
...  | no _    =  refl

So we choose const-env x v for δ and obtain δ ⊢ ` x ↓ v with the var rule.

It remains to prove that γ `⊢ σ ↓ δ and δ ⊢ M ↓ v for any k, given that we have chosen const-env x v for δ. We shall have two cases to consider, x ≡ y or x ≢ y.

Now to finish the two cases of the proof.

  • In the case where x ≡ y, we need to show that γ ⊢ σ y ↓ v, but that’s just our premise.
  • In the case where x ≢ y, we need to show that γ ⊢ σ y ↓ ⊥, which we do via rule ⊥-intro.

Thus, we have completed the variable case of the proof that simultaneous substitution reflects denotations. Here is the proof again, formally.

subst-reflect-var :  {Γ Δ} {γ : Env Δ} {x : Γ  } {v} {σ : Subst Γ Δ}
   γ  σ x  v
    -----------------------------------------
   Σ[ δ  Env Γ ] γ `⊢ σ  δ  ×  δ  ` x  v
subst-reflect-var {Γ}{Δ}{γ}{x}{v}{σ} xv
  rewrite sym (same-const-env {Γ}{x}{v}) =
     const-env x v ,  const-env-ok , var  
  where
  const-env-ok : γ `⊢ σ  const-env x v
  const-env-ok y with x var≟ y
  ... | yes x≡y rewrite sym x≡y | same-const-env {Γ}{x}{v} = xv
  ... | no x≢y rewrite diff-const-env {Γ}{x}{y}{v} x≢y = ⊥-intro

Substitutions and environment construction

Every substitution produces terms that can evaluate to .

subst-⊥ : ∀{Γ Δ}{γ : Env Δ}{σ : Subst Γ Δ}
    -----------------
   γ `⊢ σ  `⊥
subst-⊥ x = ⊥-intro

If a substitution produces terms that evaluate to the values in both γ₁ and γ₂, then those terms also evaluate to the values in γ₁ ⊔ γ₂.

subst-⊔ : ∀{Γ Δ}{γ : Env Δ}{γ₁ γ₂ : Env Γ}{σ : Subst Γ Δ}
            γ `⊢ σ  γ₁
            γ `⊢ σ  γ₂
             -------------------------
            γ `⊢ σ  (γ₁ `⊔ γ₂)
subst-⊔ γ₁-ok γ₂-ok x = ⊔-intro (γ₁-ok x) (γ₂-ok x)

The Lambda constructor is injective

lambda-inj :  {Γ} {M N : Γ ,    }
   _≡_ {A = Γ  } (ƛ M) (ƛ N)
    ---------------------------
   M  N
lambda-inj refl = refl

Simultaneous substitution reflects denotations

In this section we prove a central lemma, that substitution reflects denotations. That is, if γ ⊢ subst σ M ↓ v, then δ ⊢ M ↓ v and γ `⊢ σ ↓ δ for some δ. We shall proceed by induction on the derivation of γ ⊢ subst σ M ↓ v. This requires a minor restatement of the lemma, changing the premise to γ ⊢ L ↓ v and L ≡ subst σ M.

split :  {Γ} {M : Γ ,   } {δ : Env (Γ , )} {v}
   δ  M  v
    --------------------------
   (init δ `, last δ)  M  v
split {δ = δ} δMv rewrite init-last δ = δMv

subst-reflect :  {Γ Δ} {δ : Env Δ} {M : Γ  } {v} {L : Δ  } {σ : Subst Γ Δ}
   δ  L  v
   subst σ M  L
    ---------------------------------------
   Σ[ γ  Env Γ ] δ `⊢ σ  γ  ×  γ  M  v

subst-reflect {M = M}{σ = σ} (var {x = y}) eqL with M
... | ` x  with var {x = y}
...           | yv  rewrite sym eqL = subst-reflect-var {σ = σ} yv
subst-reflect {M = M} (var {x = y}) () | M₁ · M₂
subst-reflect {M = M} (var {x = y}) () | ƛ M′

subst-reflect {M = M}{σ = σ} (↦-elim d₁ d₂) eqL
         with M
...    | ` x with ↦-elim d₁ d₂
...    | d′ rewrite sym eqL = subst-reflect-var {σ = σ} d′
subst-reflect (↦-elim d₁ d₂) () | ƛ M′
subst-reflect{Γ}{Δ}{γ}{σ = σ} (↦-elim d₁ d₂)
   refl | M₁ · M₂
     with subst-reflect {M = M₁} d₁ refl | subst-reflect {M = M₂} d₂ refl
...     |  δ₁ ,  subst-δ₁ , m1   |  δ₂ ,  subst-δ₂ , m2   =
      δ₁ `⊔ δ₂ ,  subst-⊔ {γ₁ = δ₁}{γ₂ = δ₂}{σ = σ} subst-δ₁ subst-δ₂ ,
                    ↦-elim (⊑-env m1 (⊑-env-conj-R1 δ₁ δ₂))
                           (⊑-env m2 (⊑-env-conj-R2 δ₁ δ₂))  

subst-reflect {M = M}{σ = σ} (↦-intro d) eqL with M
...    | ` x with (↦-intro d)
...             | d′ rewrite sym eqL = subst-reflect-var {σ = σ} d′
subst-reflect {σ = σ} (↦-intro d) eq | ƛ M′
      with subst-reflect {σ = exts σ} d (lambda-inj eq)
... |  δ′ ,  exts-σ-δ′ , m′   =
     init δ′ ,  ((λ x  rename-inc-reflect (exts-σ-δ′ (S x)))) ,
             ↦-intro (up-env (split m′) (var-inv (exts-σ-δ′ Z)))  
subst-reflect (↦-intro d) () | M₁ · M₂

subst-reflect {σ = σ} ⊥-intro eq =
     `⊥ ,  subst-⊥ {σ = σ} , ⊥-intro  

subst-reflect {σ = σ} (⊔-intro d₁ d₂) eq
  with subst-reflect {σ = σ} d₁ eq | subst-reflect {σ = σ} d₂ eq
... |  δ₁ ,  subst-δ₁ , m1   |  δ₂ ,  subst-δ₂ , m2   =
      δ₁ `⊔ δ₂ ,  subst-⊔ {γ₁ = δ₁}{γ₂ = δ₂}{σ = σ} subst-δ₁ subst-δ₂ ,
                    ⊔-intro (⊑-env m1 (⊑-env-conj-R1 δ₁ δ₂))
                            (⊑-env m2 (⊑-env-conj-R2 δ₁ δ₂))  
subst-reflect (sub d lt) eq
    with subst-reflect d eq
... |  δ ,  subst-δ , m   =  δ ,  subst-δ , sub m lt  
  • Case var: We have subst σ M ≡ y, so M must also be a variable, say x. We apply the lemma subst-reflect-var to conclude.

  • Case ↦-elim: We have subst σ M ≡ L₁ · L₂. We proceed by cases on M.

    • Case M ≡ ` x: We apply the subst-reflect-var lemma again to conclude.

    • Case M ≡ M₁ · M₂: By the induction hypothesis, we have some δ₁ and δ₂ such that δ₁ ⊢ M₁ ↓ v₁ ↦ v₃ and γ `⊢ σ ↓ δ₁, as well as δ₂ ⊢ M₂ ↓ v₁ and γ `⊢ σ ↓ δ₂. By ⊑-env we have δ₁ ⊔ δ₂ ⊢ M₁ ↓ v₁ ↦ v₃ and δ₁ ⊔ δ₂ ⊢ M₂ ↓ v₁ (using ⊑-env-conj-R1 and ⊑-env-conj-R2), and therefore δ₁ ⊔ δ₂ ⊢ M₁ · M₂ ↓ v₃. We conclude this case by obtaining γ `⊢ σ ↓ δ₁ ⊔ δ₂ by the subst-⊔ lemma.

  • Case ↦-intro: We have subst σ M ≡ ƛ L′. We proceed by cases on M.

    • Case M ≡ x: We apply the subst-reflect-var lemma.

    • Case M ≡ ƛ M′: By the induction hypothesis, we have (δ′ , v′) ⊢ M′ ↓ v₂ and (δ , v₁) ⊢ exts σ ↓ (δ′ , v′). From the later we have (δ , v₁) ⊢ # 0 ↓ v′. By the lemma var-inv we have v′ ⊑ v₁, so by the up-env lemma we have (δ′ , v₁) ⊢ M′ ↓ v₂ and therefore δ′ ⊢ ƛ M′ ↓ v₁ → v₂. We also need to show that δ ⊢ σ ↓ δ′. Fix k. We have (δ , v₁) ⊢ rename S_ σ k ↓ δ k′. We then apply the lemma rename-inc-reflect to obtain δ ⊢ σ k ↓ δ k′, so this case is complete.

  • Case ⊥-intro: We choose for δ. We have ⊥ ⊢ M ↓ ⊥ by ⊥-intro. We have δ `⊢ σ ↓ `⊥ by the lemma subst-⊥.

  • Case ⊔-intro: By the induction hypothesis we have δ₁ ⊢ M ↓ v₁, δ₂ ⊢ M ↓ v₂, δ `⊢ σ ↓ δ₁, and δ `⊢ σ ↓ δ₂. We have δ₁ ⊔ δ₂ ⊢ M ↓ v₁ and δ₁ ⊔ δ₂ ⊢ M ↓ v₂ by ⊑-env with ⊑-env-conj-R1 and ⊑-env-conj-R2. So by ⊔-intro we have δ₁ ⊔ δ₂ ⊢ M ↓ v₁ ⊔ v₂. By subst-⊔ we conclude that δ `⊢ σ ↓ δ₁ ⊔ δ₂.

Single substitution reflects denotations

Most of the work is now behind us. We have proved that simultaneous substitution reflects denotations. Of course, β reduction uses single substitution, so we need a corollary that proves that single substitution reflects denotations. That is, given terms N : (Γ , ★ ⊢ ★) and M : (Γ ⊢ ★), if γ ⊢ N [ M ] ↓ w, then γ ⊢ M ↓ v and (γ , v) ⊢ N ↓ w for some value v. We have N [ M ] = subst (subst-zero M) N.

We first prove a lemma about subst-zero, that if δ ⊢ subst-zero M ↓ γ, then γ `⊑ (δ , w) × δ ⊢ M ↓ w for some w.

subst-zero-reflect :  {Δ} {δ : Env Δ} {γ : Env (Δ , )} {M : Δ  }
   δ `⊢ subst-zero M  γ
    ----------------------------------------
   Σ[ w  Value ] γ `⊑ (δ `, w) × δ  M  w
subst-zero-reflect {δ = δ} {γ = γ} δσγ =  last γ ,  lemma , δσγ Z  
  where
  lemma : γ `⊑ (δ `, last γ)
  lemma Z  =  ⊑-refl
  lemma (S x) = var-inv (δσγ (S x))

We choose w to be the last value in γ and we obtain δ ⊢ M ↓ w by applying the premise to variable Z. Finally, to prove γ `⊑ (δ , w), we prove a lemma by induction in the input variable. The base case is trivial because of our choice of w. In the induction case, S x, the premise δ ⊢ subst-zero M ↓ γ gives us δ ⊢ x ↓ γ (S x) and then using var-inv we conclude that γ (S x) ⊑ (δ `, w) (S x).

Now to prove that substitution reflects denotations.

substitution-reflect :  {Δ} {δ : Env Δ} {N : Δ ,   } {M : Δ  } {v}
   δ  N [ M ]  v
    ------------------------------------------------
   Σ[ w  Value ] δ  M  w  ×  (δ `, w)  N  v
substitution-reflect d with subst-reflect d refl
...  |  γ ,  δσγ , γNv   with subst-zero-reflect δσγ
...    |  w ,  ineq , δMw   =  w ,  δMw , ⊑-env γNv ineq  

We apply the subst-reflect lemma to obtain δ ⊢ subst-zero M ↓ γ and γ ⊢ N ↓ v for some γ. Using the former, the subst-zero-reflect lemma gives us γ `⊑ (δ , w) and δ ⊢ M ↓ w. We conclude that δ , w ⊢ N ↓ v by applying the ⊑-env lemma, using γ ⊢ N ↓ v and γ `⊑ (δ , w).

Reduction reflects denotations

Now that we have proved that substitution reflects denotations, we can easily prove that reduction does too.

reflect-beta : ∀{Γ}{γ : Env Γ}{M N}{v}
     γ  (N [ M ])  v
     γ  (ƛ N) · M  v
reflect-beta d
    with substitution-reflect d
... |  v₂′ ,  d₁′ , d₂′   = ↦-elim (↦-intro d₂′) d₁′


reflect :  {Γ} {γ : Env Γ} {M M′ N v}
   γ  N  v    M —→ M′     M′  N
    ---------------------------------
   γ  M  v
reflect var (ξ₁ r) ()
reflect var (ξ₂ r) ()
reflect{γ = γ} (var{x = x}) β mn
    with var{γ = γ}{x = x}
... | d′ rewrite sym mn = reflect-beta d′
reflect var (ζ r) ()
reflect (↦-elim d₁ d₂) (ξ₁ r) refl = ↦-elim (reflect d₁ r refl) d₂
reflect (↦-elim d₁ d₂) (ξ₂ r) refl = ↦-elim d₁ (reflect d₂ r refl)
reflect (↦-elim d₁ d₂) β mn
    with ↦-elim d₁ d₂
... | d′ rewrite sym mn = reflect-beta d′
reflect (↦-elim d₁ d₂) (ζ r) ()
reflect (↦-intro d) (ξ₁ r) ()
reflect (↦-intro d) (ξ₂ r) ()
reflect (↦-intro d) β mn
    with ↦-intro d
... | d′ rewrite sym mn = reflect-beta d′
reflect (↦-intro d) (ζ r) refl = ↦-intro (reflect d r refl)
reflect ⊥-intro r mn = ⊥-intro
reflect (⊔-intro d₁ d₂) r mn rewrite sym mn =
   ⊔-intro (reflect d₁ r refl) (reflect d₂ r refl)
reflect (sub d lt) r mn = sub (reflect d r mn) lt

Reduction implies denotational equality

We have proved that reduction both preserves and reflects denotations. Thus, reduction implies denotational equality.

reduce-equal :  {Γ} {M : Γ  } {N : Γ  }
   M —→ N
    ---------
    M   N
reduce-equal {Γ}{M}{N} r γ v =
      m  preserve m r) ,  n  reflect n r refl) 

We conclude with the soundness property, that multi-step reduction to a lambda abstraction implies denotational equivalence with a lambda abstraction.

soundness : ∀{Γ} {M : Γ  } {N : Γ ,   }
   M —↠ ƛ N
    -----------------
    M   (ƛ N)
soundness (.(ƛ _) ) γ v =   x  x) ,  x  x) 
soundness {Γ} (L —→⟨ r  M—↠N) γ v =
   let ih = soundness M—↠N in
   let e = reduce-equal r in
   ≃-trans {Γ} e ih γ v

Unicode

This chapter uses the following unicode:

≟  U+225F  QUESTIONED EQUAL TO (\?=)

Adequacy: Adequacy of denotational semantics with respect to operational semantics

module plfa.part3.Adequacy where

Introduction

Having proved a preservation property in the last chapter, a natural next step would be to prove progress. That is, to prove a property of the form

If γ ⊢ M ↓ v, then either M is a lambda abstraction or M —→ N for some N.

Such a property would tell us that having a denotation implies either reduction to normal form or divergence. This is indeed true, but we can prove a much stronger property! In fact, having a denotation that is a function value (not ) implies reduction to a lambda abstraction.

This stronger property, reformulated a bit, is known as adequacy. That is, if a term M is denotationally equal to a lambda abstraction, then M reduces to a lambda abstraction.

ℰ M ≃ ℰ (ƛ N)  implies M —↠ ƛ N' for some N'

Recall that ℰ M ≃ ℰ (ƛ N) is equivalent to saying that γ ⊢ M ↓ (v ↦ w) for some v and w. We will show that γ ⊢ M ↓ (v ↦ w) implies multi-step reduction a lambda abstraction. The recursive structure of the derivations for γ ⊢ M ↓ (v ↦ w) are completely different from the structure of multi-step reductions, so a direct proof would be challenging. However, The structure of γ ⊢ M ↓ (v ↦ w) closer to that of BigStep call-by-name evaluation. Further, we already proved that big-step evaluation implies multi-step reduction to a lambda (cbn→reduce). So we shall prove that γ ⊢ M ↓ (v ↦ w) implies that γ' ⊢ M ⇓ c, where c is a closure (a term paired with an environment), γ' is an environment that maps variables to closures, and γ and γ' are appropriate related. The proof will be an induction on the derivation of γ ⊢ M ↓ v, and to strengthen the induction hypothesis, we will relate semantic values to closures using a logical relation 𝕍.

The rest of this chapter is organized as follows.

  • To make the 𝕍 relation down-closed with respect to , we must loosen the requirement that M result in a function value and instead require that M result in a value that is greater than or equal to a function value. We establish several properties about being ``greater than a function’’.

  • We define the logical relation 𝕍 that relates values and closures, and extend it to a relation on terms 𝔼 and environments 𝔾. We prove several lemmas that culminate in the property that if 𝕍 v c and v′ ⊑ v, then 𝕍 v′ c.

  • We prove the main lemma, that if 𝔾 γ γ' and γ ⊢ M ↓ v, then 𝔼 v (clos M γ').

  • We prove adequacy as a corollary to the main lemma.

Imports

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; _≢_; refl; trans; sym; cong; cong₂; cong-app)
open import Data.Product using (_×_; Σ; Σ-syntax; ; ∃-syntax; proj₁; proj₂)
  renaming (_,_ to ⟨_,_⟩)
open import Data.Sum
open import Relation.Nullary using (¬_)
open import Relation.Nullary.Negation using (contradiction)
open import Data.Empty using (⊥-elim) renaming ( to Bot)
open import Data.Unit
open import Relation.Nullary using (Dec; yes; no)
open import Function using (_∘_)
open import plfa.part2.Untyped
     using (Context; _⊢_; ; _∋_; ; _,_; Z; S_; `_; ƛ_; _·_;
            rename; subst; ext; exts; _[_]; subst-zero;
            _—↠_; _—→⟨_⟩_; _∎; _—→_; ξ₁; ξ₂; β; ζ)
open import plfa.part2.Substitution using (ids; sub-id)
open import plfa.part2.BigStep
     using (Clos; clos; ClosEnv; ∅'; _,'_; _⊢_⇓_; ⇓-var; ⇓-lam; ⇓-app;
            ⇓-determ; cbn→reduce)
open import plfa.part3.Denotational
     using (Value; Env; `∅; _`,_; _↦_; _⊑_; _⊢_↓_; ; all-funs∈; _⊔_; ∈→⊑;
            var; ↦-elim; ↦-intro; ⊔-intro; ⊥-intro; sub; ; _≃_; _iff_;
            ⊑-trans; ⊑-conj-R1; ⊑-conj-R2; ⊑-conj-L; ⊑-refl; ⊑-fun; ⊑-bot;
            ⊑-dist; sub-inv-fun)
open import plfa.part3.Soundness using (soundness)

The property of being greater or equal to a function

We define the following short-hand for saying that a value is greater-than or equal to a function value.

above-fun : Value  Set
above-fun u = Σ[ v  Value ] Σ[ w  Value ] v  w  u

If a value u is greater than a function, then an even greater value u' is too.

above-fun-⊑ : ∀{u u' : Value}
       above-fun u  u  u'
        -------------------
       above-fun u'
above-fun-⊑  v ,  w , lt'   lt =  v ,  w , ⊑-trans lt' lt  

The bottom value is not greater than a function.

above-fun⊥ : ¬ above-fun 
above-fun⊥  v ,  w , lt  
    with sub-inv-fun lt
... |  Γ ,  f ,  Γ⊆⊥ ,  lt1 , lt2    
    with all-funs∈ f
... |  A ,  B , m  
    with Γ⊆⊥ m
... | ()

If the join of two values u and u' is greater than a function, then at least one of them is too.

above-fun-⊔ : ∀{u u'}
            above-fun (u  u')
            above-fun u  above-fun u'
above-fun-⊔{u}{u'}  v ,  w , v↦w⊑u⊔u'  
    with sub-inv-fun v↦w⊑u⊔u'
... |  Γ ,  f ,  Γ⊆u⊔u' ,  lt1 , lt2    
    with all-funs∈ f
... |  A ,  B , m  
    with Γ⊆u⊔u' m
... | inj₁ x = inj₁  A ,  B , (∈→⊑ x)  
... | inj₂ x = inj₂  A ,  B , (∈→⊑ x)  

On the other hand, if neither of u and u' is greater than a function, then their join is also not greater than a function.

not-above-fun-⊔ : ∀{u u' : Value}
                ¬ above-fun u  ¬ above-fun u'
                ¬ above-fun (u  u')
not-above-fun-⊔ naf1 naf2 af12
    with above-fun-⊔ af12
... | inj₁ af1 = contradiction af1 naf1
... | inj₂ af2 = contradiction af2 naf2

The converse is also true. If the join of two values is not above a function, then neither of them is individually.

not-above-fun-⊔-inv : ∀{u u' : Value}  ¬ above-fun (u  u')
               ¬ above-fun u × ¬ above-fun u'
not-above-fun-⊔-inv af =  f af , g af 
  where
    f : ∀{u u' : Value}  ¬ above-fun (u  u')  ¬ above-fun u
    f{u}{u'} af12  v ,  w , lt   =
        contradiction  v ,  w , ⊑-conj-R1 lt   af12
    g : ∀{u u' : Value}  ¬ above-fun (u  u')  ¬ above-fun u'
    g{u}{u'} af12  v ,  w , lt   =
        contradiction  v ,  w , ⊑-conj-R2 lt   af12

The property of being greater than a function value is decidable, as exhibited by the following function.

above-fun? : (v : Value)  Dec (above-fun v)
above-fun?  = no above-fun⊥
above-fun? (v  w) = yes  v ,  w , ⊑-refl  
above-fun? (u  u')
    with above-fun? u | above-fun? u'
... | yes  v ,  w , lt   | _ = yes  v ,  w , (⊑-conj-R1 lt)  
... | no _ | yes  v ,  w , lt   = yes  v ,  w , (⊑-conj-R2 lt)  
... | no x | no y = no (not-above-fun-⊔ x y)

Relating values to closures

Next we relate semantic values to closures. The relation 𝕍 is for closures whose term is a lambda abstraction, i.e., in weak-head normal form (WHNF). The relation 𝔼 is for any closure. Roughly speaking, 𝔼 v c will hold if, when v is greater than a function value, c evaluates to a closure c' in WHNF and 𝕍 v c'. Regarding 𝕍 v c, it will hold when c is in WHNF, and if v is a function, the body of c evaluates according to v.

𝕍 : Value  Clos  Set
𝔼 : Value  Clos  Set

We define 𝕍 as a function from values and closures to Set and not as a data type because it is mutually recursive with 𝔼 in a negative position (to the left of an implication). We first perform case analysis on the term in the closure. If the term is a variable or application, then 𝕍 is false (Bot). If the term is a lambda abstraction, we define 𝕍 by recursion on the value, which we describe below.

𝕍 v (clos (` x₁) γ) = Bot
𝕍 v (clos (M · M₁) γ) = Bot
𝕍  (clos (ƛ M) γ) = 
𝕍 (v  w) (clos (ƛ N) γ) =
    (∀{c : Clos}  𝔼 v c  above-fun w  Σ[ c'  Clos ]
        (γ ,' c)  N  c'  ×  𝕍 w c')
𝕍 (u  v) (clos (ƛ N) γ) = 𝕍 u (clos (ƛ N) γ) × 𝕍 v (clos (ƛ N) γ)
  • If the value is , then the result is true ().

  • If the value is a join (u ⊔ v), then the result is the pair (conjunction) of 𝕍 is true for both u and v.

  • The important case is for a function value v ↦ w and closure clos (ƛ N) γ. Given any closure c such that 𝔼 v c, if w is greater than a function, then N evaluates (with γ extended with c) to some closure c' and we have 𝕍 w c'.

The definition of 𝔼 is straightforward. If v is a greater than a function, then M evaluates to a closure related to v.

𝔼 v (clos M γ') = above-fun v  Σ[ c  Clos ] γ'  M  c × 𝕍 v c

The proof of the main lemma is by induction on γ ⊢ M ↓ v, so it goes underneath lambda abstractions and must therefore reason about open terms (terms with variables). So we must relate environments of semantic values to environments of closures. In the following, 𝔾 relates γ to γ' if the corresponding values and closures are related by 𝔼.

𝔾 : ∀{Γ}  Env Γ  ClosEnv Γ  Set
𝔾 {Γ} γ γ' = ∀{x : Γ  }  𝔼 (γ x) (γ' x)

𝔾-∅ : 𝔾 `∅ ∅'
𝔾-∅ {()}

𝔾-ext : ∀{Γ}{γ : Env Γ}{γ' : ClosEnv Γ}{v c}
       𝔾 γ γ'  𝔼 v c  𝔾 (γ `, v) (γ' ,' c)
𝔾-ext {Γ} {γ} {γ'} g e {Z} = e
𝔾-ext {Γ} {γ} {γ'} g e {S x} = g

We need a few properties of the 𝕍 and 𝔼 relations. The first is that a closure in the 𝕍 relation must be in weak-head normal form. We define WHNF has follows.

data WHNF :  {Γ A}  Γ  A  Set where
  ƛ_ :  {Γ} {N : Γ ,   }
      WHNF (ƛ N)

The proof goes by cases on the term in the closure.

𝕍→WHNF : ∀{Γ}{γ : ClosEnv Γ}{M : Γ  }{v}
        𝕍 v (clos M γ)  WHNF M
𝕍→WHNF {M = ` x} {v} ()
𝕍→WHNF {M = ƛ N} {v} vc = ƛ_
𝕍→WHNF {M = L · M} {v} ()

Next we have an introduction rule for 𝕍 that mimics the ⊔-intro rule. If both u and v are related to a closure c, then their join is too.

𝕍⊔-intro : ∀{c u v}
          𝕍 u c  𝕍 v c
           ---------------
          𝕍 (u  v) c
𝕍⊔-intro {clos (` x) γ} () vc
𝕍⊔-intro {clos (ƛ N) γ} uc vc =  uc , vc 
𝕍⊔-intro {clos (L · M) γ} () vc

In a moment we prove that 𝕍 is preserved when going from a greater value to a lesser value: if 𝕍 v c and v' ⊑ v, then 𝕍 v' c. This property, named sub-𝕍, is needed by the main lemma in the case for the sub rule.

To prove sub-𝕍, we in turn need the following property concerning values that are not greater than a function, that is, values that are equivalent to . In such cases, 𝕍 v (clos (ƛ N) γ') is trivially true.

not-above-fun-𝕍 : ∀{v : Value}{Γ}{γ' : ClosEnv Γ}{N : Γ ,    }
     ¬ above-fun v
      -------------------
     𝕍 v (clos (ƛ N) γ')
not-above-fun-𝕍 {} af = tt
not-above-fun-𝕍 {v  v'} af = contradiction  v ,  v' , ⊑-refl   af
not-above-fun-𝕍 {v₁  v₂} af
    with not-above-fun-⊔-inv af
... |  af1 , af2  =  not-above-fun-𝕍 af1 , not-above-fun-𝕍 af2 

The proofs of sub-𝕍 and sub-𝔼 are intertwined.

sub-𝕍 : ∀{c : Clos}{v v'}  𝕍 v c  v'  v  𝕍 v' c
sub-𝔼 : ∀{c : Clos}{v v'}  𝔼 v c  v'  v  𝔼 v' c

We prove sub-𝕍 by case analysis on the closure’s term, to dispatch the cases for variables and application. We then proceed by induction on v' ⊑ v. We describe each case below.

sub-𝕍 {clos (` x) γ} {v} () lt
sub-𝕍 {clos (L · M) γ} () lt
sub-𝕍 {clos (ƛ N) γ} vc ⊑-bot = tt
sub-𝕍 {clos (ƛ N) γ} vc (⊑-conj-L lt1 lt2) =  (sub-𝕍 vc lt1) , sub-𝕍 vc lt2 
sub-𝕍 {clos (ƛ N) γ}  vv1 , vv2  (⊑-conj-R1 lt) = sub-𝕍 vv1 lt
sub-𝕍 {clos (ƛ N) γ}  vv1 , vv2  (⊑-conj-R2 lt) = sub-𝕍 vv2 lt
sub-𝕍 {clos (ƛ N) γ} vc (⊑-trans{v = v₂} lt1 lt2) = sub-𝕍 (sub-𝕍 vc lt2) lt1
sub-𝕍 {clos (ƛ N) γ} vc (⊑-fun lt1 lt2) ev1 sf
    with vc (sub-𝔼 ev1 lt1) (above-fun-⊑ sf lt2)
... |  c ,  Nc , v4   =  c ,  Nc , sub-𝕍 v4 lt2  
sub-𝕍 {clos (ƛ N) γ} {v  w  v  w'}  vcw , vcw'  ⊑-dist ev1c sf
    with above-fun? w | above-fun? w'
... | yes af2 | yes af3
    with vcw ev1c af2 | vcw' ev1c af3
... |  clos L δ ,  L⇓c₂ , 𝕍w  
    |  c₃ ,  L⇓c₃ , 𝕍w'   rewrite ⇓-determ L⇓c₃ L⇓c₂ with 𝕍→WHNF 𝕍w
... | ƛ_ =
       clos L δ ,  L⇓c₂ ,  𝕍w , 𝕍w'   
sub-𝕍 {c} {v  w  v  w'}  vcw , vcw'   ⊑-dist ev1c sf
    | yes af2 | no naf3
    with vcw ev1c af2
... |  clos {Γ'} L γ₁ ,  L⇓c2 , 𝕍w  
    with 𝕍→WHNF 𝕍w
... | ƛ_ {N = N'} =
      let 𝕍w' = not-above-fun-𝕍{w'}{Γ'}{γ₁}{N'} naf3 in
       clos (ƛ N') γ₁ ,  L⇓c2 , 𝕍⊔-intro 𝕍w 𝕍w'  
sub-𝕍 {c} {v  w  v  w'}  vcw , vcw'  ⊑-dist ev1c sf
    | no naf2 | yes af3
    with vcw' ev1c af3
... |  clos {Γ'} L γ₁ ,  L⇓c3 , 𝕍w'c  
    with 𝕍→WHNF 𝕍w'c
... | ƛ_ {N = N'} =
      let 𝕍wc = not-above-fun-𝕍{w}{Γ'}{γ₁}{N'} naf2 in
       clos (ƛ N') γ₁ ,  L⇓c3 , 𝕍⊔-intro 𝕍wc 𝕍w'c  
sub-𝕍 {c} {v  w  v  w'}  vcw , vcw'  ⊑-dist ev1c  v' ,  w'' , lt  
    | no naf2 | no naf3
    with above-fun-⊔  v' ,  w'' , lt  
... | inj₁ af2 = contradiction af2 naf2
... | inj₂ af3 = contradiction af3 naf3
  • Case ⊑-bot. We immediately have 𝕍 ⊥ (clos (ƛ N) γ).

  • Case ⊑-conj-L.

      v₁' ⊑ v     v₂' ⊑ v
      -------------------
      (v₁' ⊔ v₂') ⊑ v

    The induction hypotheses gives us 𝕍 v₁' (clos (ƛ N) γ) and 𝕍 v₂' (clos (ƛ N) γ), which is all we need for this case.

  • Case ⊑-conj-R1.

      v' ⊑ v₁
      -------------
      v' ⊑ (v₁ ⊔ v₂)

    The induction hypothesis gives us 𝕍 v' (clos (ƛ N) γ).

  • Case ⊑-conj-R2.

      v' ⊑ v₂
      -------------
      v' ⊑ (v₁ ⊔ v₂)

    Again, the induction hypothesis gives us 𝕍 v' (clos (ƛ N) γ).

  • Case ⊑-trans.

      v' ⊑ v₂   v₂ ⊑ v
      -----------------
           v' ⊑ v

    The induction hypothesis for v₂ ⊑ v gives us 𝕍 v₂ (clos (ƛ N) γ). We apply the induction hypothesis for v' ⊑ v₂ to conclude that 𝕍 v' (clos (ƛ N) γ).

  • Case ⊑-dist. This case is the most difficult. We have

      𝕍 (v ↦ w) (clos (ƛ N) γ)
      𝕍 (v ↦ w') (clos (ƛ N) γ)

    and need to show that

      𝕍 (v ↦ (w ⊔ w')) (clos (ƛ N) γ)

    Let c be an arbitrary closure such that 𝔼 v c. Assume w ⊔ w' is greater than a function. Unfortunately, this does not mean that both w and w' are above functions. But thanks to the lemma above-fun-⊔, we know that at least one of them is greater than a function.

    • Suppose both of them are greater than a function. Then we have γ ⊢ N ⇓ clos L δ and 𝕍 w (clos L δ). We also have γ ⊢ N ⇓ c₃ and 𝕍 w' c₃. Because the big-step semantics is deterministic, we have c₃ ≡ clos L δ. Also, from 𝕍 w (clos L δ) we know that L ≡ ƛ N' for some N'. We conclude that 𝕍 (w ⊔ w') (clos (ƛ N') δ).

    • Suppose one of them is greater than a function and the other is not: say above-fun w and ¬ above-fun w'. Then from 𝕍 (v ↦ w) (clos (ƛ N) γ) we have γ ⊢ N ⇓ clos L γ₁ and 𝕍 w (clos L γ₁). From this we have L ≡ ƛ N' for some N'. Meanwhile, from ¬ above-fun w' we have 𝕍 w' (clos L γ₁). We conclude that 𝕍 (w ⊔ w') (clos (ƛ N') γ₁).

The proof of sub-𝔼 is direct and explained below.

sub-𝔼 {clos M γ} {v} {v'} 𝔼v v'⊑v fv'
    with 𝔼v (above-fun-⊑ fv' v'⊑v)
... |  c ,  M⇓c , 𝕍v   =
       c ,  M⇓c , sub-𝕍 𝕍v v'⊑v  

From above-fun v' and v' ⊑ v we have above-fun v. Then with 𝔼 v c we obtain a closure c such that γ ⊢ M ⇓ c and 𝕍 v c. We conclude with an application of sub-𝕍 with v' ⊑ v to show 𝕍 v' c.

Programs with function denotation terminate via call-by-name

The main lemma proves that if a term has a denotation that is above a function, then it terminates via call-by-name. More formally, if γ ⊢ M ↓ v and 𝔾 γ γ', then 𝔼 v (clos M γ'). The proof is by induction on the derivation of γ ⊢ M ↓ v we discuss each case below.

The following lemma, kth-x, is used in the case for the var rule.

kth-x : ∀{Γ}{γ' : ClosEnv Γ}{x : Γ  }
      Σ[ Δ  Context ] Σ[ δ  ClosEnv Δ ] Σ[ M  Δ   ]
                 γ' x  clos M δ
kth-x{γ' = γ'}{x = x} with γ' x
... | clos{Γ = Δ} M δ =  Δ ,  δ ,  M , refl   
↓→𝔼 : ∀{Γ}{γ : Env Γ}{γ' : ClosEnv Γ}{M : Γ   }{v}
             𝔾 γ γ'  γ  M  v  𝔼 v (clos M γ')
↓→𝔼 {Γ} {γ} {γ'} 𝔾γγ' (var{x = x}) fγx
    with kth-x{Γ}{γ'}{x} | 𝔾γγ'{x = x}
... |  Δ ,  δ ,  M' , eq    | 𝔾γγ'x rewrite eq
    with 𝔾γγ'x fγx
... |  c ,  M'⇓c , 𝕍γx   =
       c ,  (⇓-var eq M'⇓c) , 𝕍γx  
↓→𝔼 {Γ} {γ} {γ'} 𝔾γγ' (↦-elim{L = L}{M = M}{v = v₁}{w = v} d₁ d₂) fv
    with ↓→𝔼 𝔾γγ' d₁  v₁ ,  v , ⊑-refl  
... |  clos L' δ ,  L⇓L' , 𝕍v₁↦v  
    with 𝕍→WHNF 𝕍v₁↦v
... | ƛ_ {N = N}
    with 𝕍v₁↦v {clos M γ'} (↓→𝔼 𝔾γγ' d₂) fv
... |  c' ,  N⇓c' , 𝕍v   =
     c' ,  ⇓-app L⇓L' N⇓c' , 𝕍v  
↓→𝔼 {Γ} {γ} {γ'} 𝔾γγ' (↦-intro{N = N}{v = v}{w = w} d) fv↦w =
     clos (ƛ N) γ' ,  ⇓-lam , E  
    where E : {c : Clos}  𝔼 v c  above-fun w
             Σ[ c'  Clos ] (γ' ,' c)  N  c'  ×  𝕍 w c'
          E {c} 𝔼vc fw = ↓→𝔼  {x}  𝔾-ext{Γ}{γ}{γ'} 𝔾γγ' 𝔼vc {x}) d fw
↓→𝔼 𝔾γγ' ⊥-intro f⊥ = ⊥-elim (above-fun⊥ f⊥)
↓→𝔼 𝔾γγ' (⊔-intro{v = v₁}{w = v₂} d₁ d₂) fv12
    with above-fun? v₁ | above-fun? v₂
... | yes fv1 | yes fv2
    with ↓→𝔼 𝔾γγ' d₁ fv1 | ↓→𝔼 𝔾γγ' d₂ fv2
... |  c₁ ,  M⇓c₁ , 𝕍v₁   |  c₂ ,  M⇓c₂ , 𝕍v₂  
    rewrite ⇓-determ M⇓c₂ M⇓c₁ =
     c₁ ,  M⇓c₁ , 𝕍⊔-intro 𝕍v₁ 𝕍v₂  
↓→𝔼 𝔾γγ' (⊔-intro{v = v₁}{w = v₂} d₁ d₂) fv12 | yes fv1 | no nfv2
    with ↓→𝔼 𝔾γγ' d₁ fv1
... |  clos {Γ'} M' γ₁ ,  M⇓c₁ , 𝕍v₁  
    with 𝕍→WHNF 𝕍v₁
... | ƛ_ {N = N} =
    let 𝕍v₂ = not-above-fun-𝕍{v₂}{Γ'}{γ₁}{N} nfv2 in
     clos (ƛ N) γ₁ ,  M⇓c₁ , 𝕍⊔-intro 𝕍v₁ 𝕍v₂  
↓→𝔼 𝔾γγ' (⊔-intro{v = v₁}{w = v₂} d₁ d₂) fv12 | no nfv1  | yes fv2
    with ↓→𝔼 𝔾γγ' d₂ fv2
... |  clos {Γ'} M' γ₁ ,  M'⇓c₂ , 𝕍2c  
    with 𝕍→WHNF 𝕍2c
... | ƛ_ {N = N} =
    let 𝕍1c = not-above-fun-𝕍{v₁}{Γ'}{γ₁}{N} nfv1 in
     clos (ƛ N) γ₁ ,  M'⇓c₂ , 𝕍⊔-intro 𝕍1c 𝕍2c  
↓→𝔼 𝔾γγ' (⊔-intro d₁ d₂) fv12 | no nfv1  | no nfv2
    with above-fun-⊔ fv12
... | inj₁ fv1 = contradiction fv1 nfv1
... | inj₂ fv2 = contradiction fv2 nfv2
↓→𝔼 {Γ} {γ} {γ'} {M} {v'} 𝔾γγ' (sub{v = v} d v'⊑v) fv'
    with ↓→𝔼 {Γ} {γ} {γ'} {M} 𝔾γγ' d (above-fun-⊑ fv' v'⊑v)
... |  c ,  M⇓c , 𝕍v   =
       c ,  M⇓c , sub-𝕍 𝕍v v'⊑v  
  • Case var. Looking up x in γ' yields some closure, clos M' δ, and from 𝔾 γ γ' we have 𝔼 (γ x) (clos M' δ). With the premise above-fun (γ x), we obtain a closure c such that δ ⊢ M' ⇓ c and 𝕍 (γ x) c. To conclude γ' ⊢ x ⇓ c via ⇓-var, we need γ' x ≡ clos M' δ, which is obvious, but it requires some Agda shananigans via the kth-x lemma to get our hands on it.

  • Case ↦-elim. We have γ ⊢ L · M ↓ v. The induction hypothesis for γ ⊢ L ↓ v₁ ↦ v gives us γ' ⊢ L ⇓ clos L' δ and 𝕍 v (clos L' δ). Of course, L' ≡ ƛ N for some N. By the induction hypothesis for γ ⊢ M ↓ v₁, we have 𝔼 v₁ (clos M γ'). Together with the premise above-fun v and 𝕍 v (clos L' δ), we obtain a closure c' such that δ ⊢ N ⇓ c' and 𝕍 v c'. We conclude that γ' ⊢ L · M ⇓ c' by rule ⇓-app.

  • Case ↦-intro. We have γ ⊢ ƛ N ↓ v ↦ w. We immediately have γ' ⊢ ƛ M ⇓ clos (ƛ M) γ' by rule ⇓-lam. But we also need to prove 𝕍 (v ↦ w) (clos (ƛ N) γ'). Let c by an arbitrary closure such that 𝔼 v c. Suppose v' is greater than a function value. We need to show that γ' , c ⊢ N ⇓ c' and 𝕍 v' c' for some c'. We prove this by the induction hypothesis for γ , v ⊢ N ↓ v' but we must first show that 𝔾 (γ , v) (γ' , c). We prove that by the lemma 𝔾-ext, using facts 𝔾 γ γ' and 𝔼 v c.

  • Case ⊥-intro. We have the premise above-fun ⊥, but that’s impossible.

  • Case ⊔-intro. We have γ ⊢ M ↓ (v₁ ⊔ v₂) and above-fun (v₁ ⊔ v₂) and need to show γ' ⊢ M ↓ c and 𝕍 (v₁ ⊔ v₂) c for some c. Again, by above-fun-⊔, at least one of v₁ or v₂ is greater than a function.

    • Suppose both v₁ and v₂ are greater than a function value. By the induction hypotheses for γ ⊢ M ↓ v₁ and γ ⊢ M ↓ v₂ we have γ' ⊢ M ⇓ c₁, 𝕍 v₁ c₁, γ' ⊢ M ⇓ c₂, and 𝕍 v₂ c₂ for some c₁ and c₂. Because is deterministic, we have c₂ ≡ c₁. Then by 𝕍⊔-intro we conclude that 𝕍 (v₁ ⊔ v₂) c₁.

    • Without loss of generality, suppose v₁ is greater than a function value but v₂ is not. By the induction hypotheses for γ ⊢ M ↓ v₁, and using 𝕍→WHNF, we have γ' ⊢ M ⇓ clos (ƛ N) γ₁ and 𝕍 v₁ (clos (ƛ N) γ₁). Then because v₂ is not greater than a function, we also have 𝕍 v₂ (clos (ƛ N) γ₁). We conclude that 𝕍 (v₁ ⊔ v₂) (clos (ƛ N) γ₁).

  • Case sub. We have γ ⊢ M ↓ v, v' ⊑ v, and above-fun v'. We need to show that γ' ⊢ M ⇓ c and 𝕍 v' c for some c. We have above-fun v by above-fun-⊑, so the induction hypothesis for γ ⊢ M ↓ v gives us a closure c such that γ' ⊢ M ⇓ c and 𝕍 v c. We conclude that 𝕍 v' c by sub-𝕍.

Proof of denotational adequacy

From the main lemma we can directly show that ℰ M ≃ ℰ (ƛ N) implies that M big-steps to a lambda, i.e., ∅ ⊢ M ⇓ clos (ƛ N′) γ.

↓→⇓ : ∀{M :   }{N :  ,   }     M   (ƛ N)
           Σ[ Γ  Context ] Σ[ N′  (Γ ,   ) ] Σ[ γ  ClosEnv Γ ]
            ∅'  M  clos (ƛ N′) γ
↓→⇓{M}{N} eq
    with ↓→𝔼 𝔾-∅ ((proj₂ (eq `∅ (  ))) (↦-intro ⊥-intro))
                   ,   , ⊑-refl  
... |  clos {Γ} M′ γ ,  M⇓c , Vc  
    with 𝕍→WHNF Vc
... | ƛ_ {N = N′} =
     Γ ,  N′ ,  γ , M⇓c    

The proof goes as follows. We derive ∅ ⊢ ƛ N ↓ ⊥ ↦ ⊥ and then ℰ M ≃ ℰ (ƛ N) gives us ∅ ⊢ M ↓ ⊥ ↦ ⊥. We conclude by applying the main lemma to obtain ∅ ⊢ M ⇓ clos (ƛ N′) γ for some N′ and γ.

Now to prove the adequacy property. We apply the above lemma to obtain ∅ ⊢ M ⇓ clos (ƛ N′) γ and then apply cbn→reduce to conclude.

adequacy : ∀{M :   }{N :  ,   }
      M   (ƛ N)
    Σ[ N′  ( ,   ) ]
     (M —↠ ƛ N′)
adequacy{M}{N} eq
    with ↓→⇓ eq
... |  Γ ,  N′ ,  γ , M⇓    =
    cbn→reduce M⇓

Call-by-name is equivalent to beta reduction

As promised, we return to the question of whether call-by-name evaluation is equivalent to beta reduction. In chapter BigStep we established the forward direction: that if call-by-name produces a result, then the program beta reduces to a lambda abstraction (cbn→reduce). We now prove the backward direction of the if-and-only-if, leveraging our results about the denotational semantics.

reduce→cbn :  {M :   } {N :  ,   }
            M —↠ ƛ N
            Σ[ Δ  Context ] Σ[ N′  Δ ,    ] Σ[ δ  ClosEnv Δ ]
             ∅'  M  clos (ƛ N′) δ
reduce→cbn M—↠ƛN = ↓→⇓ (soundness M—↠ƛN)

Suppose M —↠ ƛ N. Soundness of the denotational semantics gives us ℰ M ≃ ℰ (ƛ N). Then by ↓→⇓ we conclude that ∅' ⊢ M ⇓ clos (ƛ N′) δ for some N′ and δ.

Putting the two directions of the if-and-only-if together, we establish that call-by-name evaluation is equivalent to beta reduction in the following sense.

cbn↔reduce :  {M :   }
            (Σ[ N   ,    ] (M —↠ ƛ N))
             iff
             (Σ[ Δ  Context ] Σ[ N′  Δ ,    ] Σ[ δ  ClosEnv Δ ]
               ∅'  M  clos (ƛ N′) δ)
cbn↔reduce {M} =   x  reduce→cbn (proj₂ x)) ,
                    x  cbn→reduce (proj₂ (proj₂ (proj₂ x)))) 

Unicode

This chapter uses the following unicode:

𝔼  U+1D53C  MATHEMATICAL DOUBLE-STRUCK CAPITAL E (\bE)
𝔾  U+1D53E  MATHEMATICAL DOUBLE-STRUCK CAPITAL G (\bG)
𝕍  U+1D53E  MATHEMATICAL DOUBLE-STRUCK CAPITAL V (\bV)

ContextualEquivalence: Denotational equality implies contextual equivalence

module plfa.part3.ContextualEquivalence where

Imports

open import Data.Product using (_×_; Σ; Σ-syntax; ; ∃-syntax; proj₁; proj₂)
     renaming (_,_ to ⟨_,_⟩)
open import plfa.part2.Untyped using (_⊢_; ; ; _,_; ƛ_; _—↠_)
open import plfa.part2.BigStep using (_⊢_⇓_; cbn→reduce)
open import plfa.part3.Denotational using (; _≃_; ≃-sym; ≃-trans; _iff_)
open import plfa.part3.Compositional using (Ctx; plug; compositionality)
open import plfa.part3.Soundness using (soundness)
open import plfa.part3.Adequacy using (↓→⇓)

Contextual Equivalence

The notion of contextual equivalence is an important one for programming languages because it is the sufficient condition for changing a subterm of a program while maintaining the program’s overall behavior. Two terms M and N are contextually equivalent if they can plugged into any context C and produce equivalent results. As discuss in the Denotational chapter, the result of a program in the lambda calculus is to terminate or not. We characterize termination with the reduction semantics as follows.

terminates : ∀{Γ}  (M : Γ  )  Set
terminates {Γ} M = Σ[ N  (Γ ,   ) ] (M —↠ ƛ N)

So two terms are contextually equivalent if plugging them into the same context produces two programs that either terminate or diverge together.

_≅_ : ∀{Γ}  (M N : Γ  )  Set
(_≅_ {Γ} M N) =  {C : Ctx Γ }
                 (terminates (plug C M)) iff (terminates (plug C N))

The contextual equivalence of two terms is difficult to prove directly based on the above definition because of the universal quantification of the context C. One of the main motivations for developing denotational semantics is to have an alternative way to prove contextual equivalence that instead only requires reasoning about the two terms.

Denotational equivalence implies contextual equivalence

Thankfully, the proof that denotational equality implies contextual equivalence is an easy corollary of the results that we have already established. Furthermore, the two directions of the if-and-only-if are symmetric, so we can prove one lemma and then use it twice in the theorem.

The lemma states that if M and N are denotationally equal and if M plugged into C terminates, then so does N plugged into C.

denot-equal-terminates : ∀{Γ} {M N : Γ  } {C : Ctx Γ }
    M   N    terminates (plug C M)
    -----------------------------------
   terminates (plug C N)
denot-equal-terminates {Γ}{M}{N}{C} ℰM≃ℰN  N′ , CM—↠ƛN′  =
  let ℰCM≃ℰƛN′ = soundness CM—↠ƛN′ in
  let ℰCM≃ℰCN = compositionality{Γ = Γ}{Δ = }{C = C} ℰM≃ℰN in
  let ℰCN≃ℰƛN′ = ≃-trans (≃-sym ℰCM≃ℰCN) ℰCM≃ℰƛN′ in
    cbn→reduce (proj₂ (proj₂ (proj₂ (↓→⇓ ℰCN≃ℰƛN′))))

The proof is direct. Because plug C —↠ plug C (ƛ N′), we can apply soundness to obtain

ℰ (plug C M) ≃ ℰ (ƛ N′)

From ℰ M ≃ ℰ N, compositionality gives us

ℰ (plug C M) ≃ ℰ (plug C N).

Putting these two facts together gives us

ℰ (plug C N) ≃ ℰ (ƛ N′).

We then apply ↓→⇓ from Chapter Adequacy to deduce

∅' ⊢ plug C N ⇓ clos (ƛ N′′) δ).

Call-by-name evaluation implies reduction to a lambda abstraction, so we conclude that

terminates (plug C N).

The main theorem follows by two applications of the lemma.

denot-equal-contex-equal : ∀{Γ} {M N : Γ  }
    M   N
    ---------
   M  N
denot-equal-contex-equal{Γ}{M}{N} eq {C} =
     tm  denot-equal-terminates eq tm) ,
      tn  denot-equal-terminates (≃-sym eq) tn) 

Unicode

This chapter uses the following unicode:

≅  U+2245  APPROXIMATELY EQUAL TO (\~= or \cong)

附录

Substitution: Substitution in the untyped lambda calculus

module plfa.part2.Substitution where

Introduction

The primary purpose of this chapter is to prove that substitution commutes with itself. Barendregt (1984) refers to this as the substitution lemma:

M [x:=N] [y:=L] = M [y:=L] [x:= N[y:=L] ]

In our setting, with de Bruijn indices for variables, the statement of the lemma becomes:

M [ N ] [ L ] ≡  M〔 L 〕[ N [ L ] ]                     (substitution)

where the notation M 〔 L 〕 is for substituting L for index 1 inside M. In addition, because we define substitution in terms of parallel substitution, we have the following generalization, replacing the substitution of L with an arbitrary parallel substitution σ.

subst σ (M [ N ]) ≡ (subst (exts σ) M) [ subst σ N ]    (subst-commute)

The special case for renamings is also useful.

rename ρ (M [ N ]) ≡ (rename (ext ρ) M) [ rename ρ N ]
                                                 (rename-subst-commute)

The secondary purpose of this chapter is to define the σ algebra of parallel substitution due to Abadi, Cardelli, Curien, and Levy (1991). The equations of this algebra not only help us prove the substitution lemma, but they are generally useful. Furthermore, when the equations are applied from left to right, they form a rewrite system that decides whether any two substitutions are equal.

Imports

import Relation.Binary.PropositionalEquality as Eq
open Eq using (_≡_; refl; sym; cong; cong₂; cong-app)
open Eq.≡-Reasoning using (begin_; _≡⟨⟩_; step-≡; _∎)
open import Function using (_∘_)
open import plfa.part2.Untyped
     using (Type; Context; _⊢_; ; _∋_; ; _,_; Z; S_; `_; ƛ_; _·_;
            rename; subst; ext; exts; _[_]; subst-zero)
postulate
  extensionality :  {A B : Set} {f g : A  B}
     (∀ (x : A)  f x  g x)
      -----------------------
     f  g

Notation

We introduce the following shorthand for the type of a renaming from variables in context Γ to variables in context Δ.

Rename : Context  Context  Set
Rename Γ Δ = ∀{A}  Γ  A  Δ  A

Similarly, we introduce the following shorthand for the type of a substitution from variables in context Γ to terms in context Δ.

Subst : Context  Context  Set
Subst Γ Δ = ∀{A}  Γ  A  Δ  A

We use the following more succinct notation for the subst function.

⟪_⟫ : ∀{Γ Δ A}  Subst Γ Δ  Γ  A  Δ  A
 σ  = λ M  subst σ M

The σ algebra of substitution

A substitution maps de Bruijn indices (natural numbers) to terms, so we can view a substitution simply as a sequence of terms, or more precisely, as an infinite sequence of terms. The σ algebra consists of four operations for building such sequences: identity ids, shift , cons M • σ, and sequencing σ ⨟ τ. The sequence 0, 1, 2, ... is constructed by the identity substitution.

ids : ∀{Γ}  Subst Γ Γ
ids x = ` x

The shift operation constructs the sequence

1, 2, 3, ...

and is defined as follows.

 : ∀{Γ A}  Subst Γ (Γ , A)
 x = ` (S x)

Given a term M and substitution σ, the operation M • σ constructs the sequence

M , σ 0, σ 1, σ 2, ...

This operation is analogous to the cons operation of Lisp.

infixr 6 _•_

_•_ : ∀{Γ Δ A}  (Δ  A)  Subst Γ Δ  Subst (Γ , A) Δ
(M  σ) Z = M
(M  σ) (S x) = σ x

Given two substitutions σ and τ, the sequencing operation σ ⨟ τ composes the two substitutions by first applying σ and then applying τ. So it produces the sequence

⟪τ⟫(σ 0), ⟪τ⟫(σ 1), ⟪τ⟫(σ 2), ...

Here is the definition.

infixr 5 _⨟_

_⨟_ : ∀{Γ Δ Σ}  Subst Γ Δ  Subst Δ Σ  Subst Γ Σ
σ  τ =  τ   σ

For the sequencing operation, Abadi et al. use the notation of function composition, writing σ ∘ τ, but still with σ applied before τ, which is the opposite of standard mathematical practice. We instead write σ ⨟ τ, because semicolon is the standard notation for forward function composition.

The σ algebra equations

The σ algebra includes the following equations.

(sub-head)  ⟪ M • σ ⟫ (` Z) ≡ M
(sub-tail)  ↑ ⨟ (M • σ)    ≡ σ
(sub-η)     (⟪ σ ⟫ (` Z)) • (↑ ⨟ σ) ≡ σ
(Z-shift)   (` Z) • ↑      ≡ ids

(sub-id)    ⟪ ids ⟫ M      ≡ M
(sub-app)   ⟪ σ ⟫ (L · M)  ≡ (⟪ σ ⟫ L) · (⟪ σ ⟫ M)
(sub-abs)   ⟪ σ ⟫ (ƛ N)    ≡ ƛ ⟪ σ ⟫ N
(sub-sub)   ⟪ τ ⟫ ⟪ σ ⟫ M  ≡ ⟪ σ ⨟ τ ⟫ M

(sub-idL)   ids ⨟ σ        ≡ σ
(sub-idR)   σ ⨟ ids        ≡ σ
(sub-assoc) (σ ⨟ τ) ⨟ θ    ≡ σ ⨟ (τ ⨟ θ)
(sub-dist)  (M • σ) ⨟ τ    ≡ (⟪ τ ⟫ M) • (σ ⨟ τ)

The first group of equations describe how the operator acts like cons. The equation sub-head says that the variable zero Z returns the head of the sequence (it acts like the car of Lisp). Similarly, sub-tail says that sequencing with shift returns the tail of the sequence (it acts like cdr of Lisp). The sub-η equation is the η-expansion rule for sequences, saying that taking the head and tail of a sequence, and then cons’ing them together yields the original sequence. The Z-shift equation says that cons’ing zero onto the shifted sequence produces the identity sequence.

The next four equations involve applying substitutions to terms. The equation sub-id says that the identity substitution returns the term unchanged. The equations sub-app and sub-abs says that substitution is a congruence for the lambda calculus. The sub-sub equation says that the sequence operator behaves as intended.

The last four equations concern the sequencing of substitutions. The first two equations, sub-idL and sub-idR, say that ids is the left and right unit of the sequencing operator. The sub-assoc equation says that sequencing is associative. Finally, sub-dist says that post-sequencing distributes through cons.

Relating the σ algebra and substitution functions

The definitions of substitution N [ M ] and parallel substitution subst σ N depend on several auxiliary functions: rename, exts, ext, and subst-zero. We shall relate those functions to terms in the σ algebra.

To begin with, renaming can be expressed in terms of substitution. We have

rename ρ M ≡ ⟪ ren ρ ⟫ M               (rename-subst-ren)

where ren turns a renaming ρ into a substitution by post-composing ρ with the identity substitution.

ren : ∀{Γ Δ}  Rename Γ Δ  Subst Γ Δ
ren ρ = ids  ρ

When the renaming is the increment function, then it is equivalent to shift.

ren S_ ≡ ↑                             (ren-shift)

rename S_ M ≡ ⟪ ↑ ⟫ M                  (rename-shift)

Renaming with the identity renaming leaves the term unchanged.

rename (λ {A} x → x) M ≡ M             (rename-id)

Next we relate the exts function to the σ algebra. Recall that the exts function extends a substitution as follows:

exts σ = ` Z, rename S_ (σ 0), rename S_ (σ 1), rename S_ (σ 2), ...

So exts is equivalent to cons’ing Z onto the sequence formed by applying σ and then shifting.

exts σ ≡ ` Z • (σ ⨟ ↑)                (exts-cons-shift)

The ext function does the same job as exts but for renamings instead of substitutions. So composing ext with ren is the same as composing ren with exts.

ren (ext ρ) ≡ exts (ren ρ)            (ren-ext)

Thus, we can recast the exts-cons-shift equation in terms of renamings.

ren (ext ρ) ≡ ` Z • (ren ρ ⨟ ↑)       (ext-cons-Z-shift)

It is also useful to specialize the sub-sub equation of the σ algebra to the situation where the first substitution is a renaming.

⟪ σ ⟫ (rename ρ M) ≡ ⟪ σ ∘ ρ ⟫ M       (rename-subst)

The subst-zero M substitution is equivalent to cons’ing M onto the identity substitution.

subst-zero M ≡ M • ids                (subst-Z-cons-ids)

Finally, sequencing exts σ with subst-zero M is equivalent to cons’ing M onto σ.

exts σ ⨟ subst-zero M ≡ (M • σ)       (subst-zero-exts-cons)

Proofs of sub-head, sub-tail, sub-η, Z-shift, sub-idL, sub-dist, and sub-app

We start with the proofs that are immediate from the definitions of the operators.

sub-head :  {Γ Δ} {A} {M : Δ  A}{σ : Subst Γ Δ}
           M  σ  (` Z)  M
sub-head = refl
sub-tail : ∀{Γ Δ} {A B} {M : Δ  A} {σ : Subst Γ Δ}
          (  M  σ) {A = B}  σ
sub-tail = extensionality λ x  refl
sub-η : ∀{Γ Δ} {A B} {σ : Subst (Γ , A) Δ}
       ( σ  (` Z)  (  σ)) {A = B}  σ
sub-η {Γ}{Δ}{A}{B}{σ} = extensionality λ x  lemma
   where
   lemma :  {x}  (( σ  (` Z))  (  σ)) x  σ x
   lemma {x = Z} = refl
   lemma {x = S x} = refl
Z-shift : ∀{Γ}{A B}
         ((` Z)  )  ids {Γ , A} {B}
Z-shift {Γ}{A}{B} = extensionality lemma
   where
   lemma : (x : Γ , A  B)  ((` Z)  ) x  ids x
   lemma Z = refl
   lemma (S y) = refl
sub-idL : ∀{Γ Δ} {σ : Subst Γ Δ} {A}
        ids  σ  σ {A}
sub-idL = extensionality λ x  refl
sub-dist :  ∀{Γ Δ Σ : Context} {A B} {σ : Subst Γ Δ} {τ : Subst Δ Σ}
              {M : Δ  A}
          ((M  σ)  τ)  ((subst τ M)  (σ  τ)) {B}
sub-dist {Γ}{Δ}{Σ}{A}{B}{σ}{τ}{M} = extensionality λ x  lemma {x = x}
  where
  lemma :  {x : Γ , A  B}  ((M  σ)  τ) x  ((subst τ M)  (σ  τ)) x
  lemma {x = Z} = refl
  lemma {x = S x} = refl
sub-app : ∀{Γ Δ} {σ : Subst Γ Δ} {L : Γ  }{M : Γ  }
          σ  (L · M)   ( σ  L) · ( σ  M)
sub-app = refl

Interlude: congruences

In this section we establish congruence rules for the σ algebra operators and and for subst and its helper functions ext, rename, exts, and subst-zero. These congruence rules help with the equational reasoning in the later sections of this chapter.

cong-ext : ∀{Γ Δ}{ρ ρ′ : Rename Γ Δ}{B}
    (∀{A}  ρ  ρ′ {A})
     ---------------------------------
    ∀{A}  ext ρ {B = B}  ext ρ′ {A}
cong-ext{Γ}{Δ}{ρ}{ρ′}{B} rr {A} = extensionality λ x  lemma {x}
  where
  lemma : ∀{x : Γ , B  A}  ext ρ x  ext ρ′ x
  lemma {Z} = refl
  lemma {S y} = cong S_ (cong-app rr y)
cong-rename : ∀{Γ Δ}{ρ ρ′ : Rename Γ Δ}{B}{M : Γ  B}
         (∀{A}  ρ  ρ′ {A})
          ------------------------
         rename ρ M  rename ρ′ M
cong-rename {M = ` x} rr = cong `_ (cong-app rr x)
cong-rename {ρ = ρ} {ρ′ = ρ′} {M = ƛ N} rr =
   cong ƛ_ (cong-rename {ρ = ext ρ}{ρ′ = ext ρ′}{M = N} (cong-ext rr))
cong-rename {M = L · M} rr =
   cong₂ _·_ (cong-rename rr) (cong-rename rr)
cong-exts : ∀{Γ Δ}{σ σ′ : Subst Γ Δ}{B}
    (∀{A}  σ  σ′ {A})
     -----------------------------------
    ∀{A}  exts σ {B = B}  exts σ′ {A}
cong-exts{Γ}{Δ}{σ}{σ′}{B} ss {A} = extensionality λ x  lemma {x}
   where
   lemma : ∀{x}  exts σ x  exts σ′ x
   lemma {Z} = refl
   lemma {S x} = cong (rename S_) (cong-app (ss {A}) x)
cong-sub : ∀{Γ Δ}{σ σ′ : Subst Γ Δ}{A}{M M′ : Γ  A}
             (∀{A}  σ  σ′ {A})    M  M′
              ------------------------------
             subst σ M  subst σ′ M′
cong-sub {Γ} {Δ} {σ} {σ′} {A} {` x} ss refl = cong-app ss x
cong-sub {Γ} {Δ} {σ} {σ′} {A} {ƛ M} ss refl =
   cong ƛ_ (cong-sub {σ = exts σ}{σ′ = exts σ′} {M = M} (cong-exts ss) refl)
cong-sub {Γ} {Δ} {σ} {σ′} {A} {L · M} ss refl =
   cong₂ _·_ (cong-sub {M = L} ss refl) (cong-sub {M = M} ss refl)
cong-sub-zero : ∀{Γ}{B : Type}{M M′ : Γ  B}
   M  M′
    -----------------------------------------
   ∀{A}  subst-zero M  (subst-zero M′) {A}
cong-sub-zero {Γ}{B}{M}{M′} mm' {A} =
   extensionality λ x  cong  z  subst-zero z x) mm'
cong-cons : ∀{Γ Δ}{A}{M N : Δ  A}{σ τ : Subst Γ Δ}
   M  N    (∀{A}  σ {A}  τ {A})
    --------------------------------
   ∀{A}  (M  σ) {A}  (N  τ) {A}
cong-cons{Γ}{Δ}{A}{M}{N}{σ}{τ} refl st {A′} = extensionality lemma
  where
  lemma : (x : Γ , A  A′)  (M  σ) x  (M  τ) x
  lemma Z = refl
  lemma (S x) = cong-app st x
cong-seq : ∀{Γ Δ Σ}{σ σ′ : Subst Γ Δ}{τ τ′ : Subst Δ Σ}
   (∀{A}  σ {A}  σ′ {A})  (∀{A}  τ {A}  τ′ {A})
   ∀{A}  (σ  τ) {A}  (σ′  τ′) {A}
cong-seq {Γ}{Δ}{Σ}{σ}{σ′}{τ}{τ′} ss' tt' {A} = extensionality lemma
  where
  lemma : (x : Γ  A)  (σ  τ) x  (σ′  τ′) x
  lemma x =
     begin
       (σ  τ) x
     ≡⟨⟩
       subst τ (σ x)
     ≡⟨ cong (subst τ) (cong-app ss' x) 
       subst τ (σ′ x)
     ≡⟨ cong-sub{M = σ′ x} tt' refl 
       subst τ′ (σ′ x)
     ≡⟨⟩
       (σ′  τ′) x
     

Relating rename, exts, ext, and subst-zero to the σ algebra

In this section we establish equations that relate subst and its helper functions (rename, exts, ext, and subst-zero) to terms in the σ algebra.

The first equation we prove is

rename ρ M ≡ ⟪ ren ρ ⟫ M              (rename-subst-ren)

Because subst uses the exts function, we need the following lemma which says that exts and ext do the same thing except that ext works on renamings and exts works on substitutions.

ren-ext :  {Γ Δ}{B C : Type} {ρ : Rename Γ Δ}
         ren (ext ρ {B = B})  exts (ren ρ) {C}
ren-ext {Γ}{Δ}{B}{C}{ρ} = extensionality λ x  lemma {x = x}
  where
  lemma :  {x : Γ , B  C}  (ren (ext ρ)) x  exts (ren ρ) x
  lemma {x = Z} = refl
  lemma {x = S x} = refl

With this lemma in hand, the proof is a straightforward induction on the term M.

rename-subst-ren :  {Γ Δ}{A} {ρ : Rename Γ Δ}{M : Γ  A}
                  rename ρ M   ren ρ  M
rename-subst-ren {M = ` x} = refl
rename-subst-ren {ρ = ρ}{M = ƛ N} =
  begin
      rename ρ (ƛ N)
    ≡⟨⟩
      ƛ rename (ext ρ) N
    ≡⟨ cong ƛ_ (rename-subst-ren {ρ = ext ρ}{M = N}) 
      ƛ  ren (ext ρ)  N
    ≡⟨ cong ƛ_ (cong-sub {M = N} ren-ext refl) 
      ƛ  exts (ren ρ)   N
    ≡⟨⟩
       ren ρ  (ƛ N)
  
rename-subst-ren {M = L · M} = cong₂ _·_ rename-subst-ren rename-subst-ren

The substitution ren S_ is equivalent to .

ren-shift : ∀{Γ}{A}{B}
           ren S_   {A = B} {A}
ren-shift {Γ}{A}{B} = extensionality λ x  lemma {x = x}
  where
  lemma :  {x : Γ  A}  ren (S_{B = B}) x   {A = B} x
  lemma {x = Z} = refl
  lemma {x = S x} = refl

The substitution rename S_ M is equivalent to shifting: ⟪ ↑ ⟫ M.

rename-shift : ∀{Γ} {A} {B} {M : Γ  A}
              rename (S_{B = B}) M     M
rename-shift{Γ}{A}{B}{M} =
  begin
    rename S_ M
  ≡⟨ rename-subst-ren 
     ren S_  M
  ≡⟨ cong-sub{M = M} ren-shift refl 
       M
  

Next we prove the equation exts-cons-shift, which states that exts is equivalent to cons’ing Z onto the sequence formed by applying σ and then shifting. The proof is by case analysis on the variable x, using rename-subst-ren for when x = S y.

exts-cons-shift : ∀{Γ Δ} {A B} {σ : Subst Γ Δ}
                 exts σ {A} {B}  (` Z  (σ  ))
exts-cons-shift = extensionality λ x  lemma{x = x}
  where
  lemma : ∀{Γ Δ} {A B} {σ : Subst Γ Δ} {x : Γ , B  A}
                   exts σ x  (` Z  (σ  )) x
  lemma {x = Z} = refl
  lemma {x = S y} = rename-subst-ren

As a corollary, we have a similar correspondence for ren (ext ρ).

ext-cons-Z-shift : ∀{Γ Δ} {ρ : Rename Γ Δ}{A}{B}
                  ren (ext ρ {B = B})  (` Z  (ren ρ  )) {A}
ext-cons-Z-shift {Γ}{Δ}{ρ}{A}{B} =
  begin
    ren (ext ρ)
  ≡⟨ ren-ext 
    exts (ren ρ)
  ≡⟨ exts-cons-shift{σ = ren ρ} 
   ((` Z)  (ren ρ  ))
  

Finally, the subst-zero M substitution is equivalent to cons’ing M onto the identity substitution.

subst-Z-cons-ids : ∀{Γ}{A B : Type}{M : Γ  B}
                  subst-zero M  (M  ids) {A}
subst-Z-cons-ids = extensionality λ x  lemma {x = x}
  where
  lemma : ∀{Γ}{A B : Type}{M : Γ  B}{x : Γ , B  A}
                       subst-zero M x  (M  ids) x
  lemma {x = Z} = refl
  lemma {x = S x} = refl

Proofs of sub-abs, sub-id, and rename-id

The equation sub-abs follows immediately from the equation exts-cons-shift.

sub-abs : ∀{Γ Δ} {σ : Subst Γ Δ} {N : Γ ,   }
          σ  (ƛ N)  ƛ  (` Z)  (σ  )  N
sub-abs {σ = σ}{N = N} =
   begin
      σ  (ƛ N)
   ≡⟨⟩
     ƛ  exts σ  N
   ≡⟨ cong ƛ_ (cong-sub{M = N} exts-cons-shift refl) 
     ƛ  (` Z)  (σ  )  N
   

The proof of sub-id requires the following lemma which says that extending the identity substitution produces the identity substitution.

exts-ids : ∀{Γ}{A B}
          exts ids  ids {Γ , B} {A}
exts-ids {Γ}{A}{B} = extensionality lemma
  where lemma : (x : Γ , B  A)  exts ids x  ids x
        lemma Z = refl
        lemma (S x) = refl

The proof of ⟪ ids ⟫ M ≡ M now follows easily by induction on M, using exts-ids in the case for M ≡ ƛ N.

sub-id : ∀{Γ} {A} {M : Γ  A}
           ids  M  M
sub-id {M = ` x} = refl
sub-id {M = ƛ N} =
   begin
      ids  (ƛ N)
   ≡⟨⟩
     ƛ  exts ids  N
   ≡⟨ cong ƛ_ (cong-sub{M = N} exts-ids refl)  
     ƛ  ids  N
   ≡⟨ cong ƛ_ sub-id 
     ƛ N
   
sub-id {M = L · M} = cong₂ _·_ sub-id sub-id

The rename-id equation is a corollary is sub-id.

rename-id :  {Γ}{A} {M : Γ  A}
   rename  {A} x  x) M  M
rename-id {M = M} =
   begin
     rename  {A} x  x) M
   ≡⟨ rename-subst-ren  
      ren  {A} x  x)  M
   ≡⟨⟩
      ids  M
   ≡⟨ sub-id  
     M
   

Proof of sub-idR

The proof of sub-idR follows directly from sub-id.

sub-idR : ∀{Γ Δ} {σ : Subst Γ Δ} {A}
        (σ  ids)  σ {A}
sub-idR {Γ}{σ = σ}{A} =
          begin
            σ  ids
          ≡⟨⟩
             ids   σ
          ≡⟨ extensionality  x  sub-id) 
            σ
          

Proof of sub-sub

The sub-sub equation states that sequenced substitutions σ ⨟ τ are equivalent to first applying σ then applying τ.

⟪ τ ⟫ ⟪ σ ⟫ M  ≡ ⟪ σ ⨟ τ ⟫ M

The proof requires several lemmas. First, we need to prove the specialization for renaming.

rename ρ (rename ρ′ M) ≡ rename (ρ ∘ ρ′) M

This in turn requires the following lemma about ext.

compose-ext : ∀{Γ Δ Σ}{ρ : Rename Δ Σ} {ρ′ : Rename Γ Δ} {A B}
             ((ext ρ)  (ext ρ′))  ext (ρ  ρ′) {B} {A}
compose-ext = extensionality λ x  lemma {x = x}
  where
  lemma : ∀{Γ Δ Σ}{ρ : Rename Δ Σ} {ρ′ : Rename Γ Δ} {A B} {x : Γ , B  A}
               ((ext ρ)  (ext ρ′)) x  ext (ρ  ρ′) x
  lemma {x = Z} = refl
  lemma {x = S x} = refl

To prove that composing renamings is equivalent to applying one after the other using rename, we proceed by induction on the term M, using the compose-ext lemma in the case for M ≡ ƛ N.

compose-rename : ∀{Γ Δ Σ}{A}{M : Γ  A}{ρ : Rename Δ Σ}{ρ′ : Rename Γ Δ}
   rename ρ (rename ρ′ M)  rename (ρ  ρ′) M
compose-rename {M = ` x} = refl
compose-rename {Γ}{Δ}{Σ}{A}{ƛ N}{ρ}{ρ′} = cong ƛ_ G
  where
  G : rename (ext ρ) (rename (ext ρ′) N)  rename (ext (ρ  ρ′)) N
  G =
      begin
        rename (ext ρ) (rename (ext ρ′) N)
      ≡⟨ compose-rename{ρ = ext ρ}{ρ′ = ext ρ′} 
        rename ((ext ρ)  (ext ρ′)) N
      ≡⟨ cong-rename compose-ext 
        rename (ext (ρ  ρ′)) N
      
compose-rename {M = L · M} = cong₂ _·_ compose-rename compose-rename

The next lemma states that if a renaming and substitution commute on variables, then they also commute on terms. We explain the proof in detail below.

commute-subst-rename : ∀{Γ Δ}{M : Γ  }{σ : Subst Γ Δ}
                        {ρ : ∀{Γ}  Rename Γ (Γ , )}
      (∀{x : Γ  }  exts σ {B = } (ρ x)  rename ρ (σ x))
      subst (exts σ {B = }) (rename ρ M)  rename ρ (subst σ M)
commute-subst-rename {M = ` x} r = r
commute-subst-rename{Γ}{Δ}{ƛ N}{σ}{ρ} r =
   cong ƛ_ (commute-subst-rename{Γ , }{Δ , }{N}
               {exts σ}{ρ = ρ′}  {x}  H {x}))
   where
   ρ′ :  {Γ}  Rename Γ (Γ , )
   ρ′ {} = λ ()
   ρ′ {Γ , } = ext ρ

   H : {x : Γ ,   }  exts (exts σ) (ext ρ x)  rename (ext ρ) (exts σ x)
   H {Z} = refl
   H {S y} =
     begin
       exts (exts σ) (ext ρ (S y))
     ≡⟨⟩
       rename S_ (exts σ (ρ y))
     ≡⟨ cong (rename S_) r 
       rename S_ (rename ρ (σ y))
     ≡⟨ compose-rename 
       rename (S_  ρ) (σ y)
     ≡⟨ cong-rename refl 
       rename ((ext ρ)  S_) (σ y)
     ≡⟨ sym compose-rename 
       rename (ext ρ) (rename S_ (σ y))
     ≡⟨⟩
       rename (ext ρ) (exts σ (S y))
     
commute-subst-rename {M = L · M}{ρ = ρ} r =
   cong₂ _·_ (commute-subst-rename{M = L}{ρ = ρ} r)
             (commute-subst-rename{M = M}{ρ = ρ} r)

The proof is by induction on the term M.

  • If M is a variable, then we use the premise to conclude.

  • If M ≡ ƛ N, we conclude using the induction hypothesis for N. However, to use the induction hypothesis, we must show that

      exts (exts σ) (ext ρ x) ≡ rename (ext ρ) (exts σ x)

    We prove this equation by cases on x.

    • If x = Z, the two sides are equal by definition.

    • If x = S y, we obtain the goal by the following equational reasoning.

        exts (exts σ) (ext ρ (S y))
      ≡ rename S_ (exts σ (ρ y))
      ≡ rename S_ (rename S_ (σ (ρ y)      (by the premise)
      ≡ rename (ext ρ) (exts σ (S y))      (by compose-rename)
      ≡ rename ((ext ρ) ∘ S_) (σ y)
      ≡ rename (ext ρ) (rename S_ (σ y))   (by compose-rename)
      ≡ rename (ext ρ) (exts σ (S y))
  • If M is an application, we obtain the goal using the induction hypothesis for each subterm.

The last lemma needed to prove sub-sub states that the exts function distributes with sequencing. It is a corollary of commute-subst-rename as described below. (It would have been nicer to prove this directly by equational reasoning in the σ algebra, but that would require the sub-assoc equation, whose proof depends on sub-sub, which in turn depends on this lemma.)

exts-seq : ∀{Γ Δ Δ′} {σ₁ : Subst Γ Δ} {σ₂ : Subst Δ Δ′}
           {A}  (exts σ₁  exts σ₂) {A}  exts (σ₁  σ₂)
exts-seq = extensionality λ x  lemma {x = x}
  where
  lemma : ∀{Γ Δ Δ′}{A}{x : Γ ,   A} {σ₁ : Subst Γ Δ}{σ₂ : Subst Δ Δ′}
      (exts σ₁  exts σ₂) x  exts (σ₁  σ₂) x
  lemma {x = Z} = refl
  lemma {A = }{x = S x}{σ₁}{σ₂} =
     begin
       (exts σ₁  exts σ₂) (S x)
     ≡⟨⟩
        exts σ₂  (rename S_ (σ₁ x))
     ≡⟨ commute-subst-rename{M = σ₁ x}{σ = σ₂}{ρ = S_} refl 
       rename S_ ( σ₂  (σ₁ x))
     ≡⟨⟩
       rename S_ ((σ₁  σ₂) x)
     

The proof proceed by cases on x.

  • If x = Z, the two sides are equal by the definition of exts and sequencing.

  • If x = S x, we unfold the first use of exts and sequencing, then apply the lemma commute-subst-rename. We conclude by the definition of sequencing.

Now we come to the proof of sub-sub, which we explain below.

sub-sub : ∀{Γ Δ Σ}{A}{M : Γ  A} {σ₁ : Subst Γ Δ}{σ₂ : Subst Δ Σ}
              σ₂  ( σ₁  M)   σ₁  σ₂  M
sub-sub {M = ` x} = refl
sub-sub {Γ}{Δ}{Σ}{A}{ƛ N}{σ₁}{σ₂} =
   begin
      σ₂  ( σ₁  (ƛ N))
   ≡⟨⟩
     ƛ  exts σ₂  ( exts σ₁  N)
   ≡⟨ cong ƛ_ (sub-sub{M = N}{σ₁ = exts σ₁}{σ₂ = exts σ₂}) 
     ƛ  exts σ₁  exts σ₂  N
   ≡⟨ cong ƛ_ (cong-sub{M = N}  {A}  exts-seq) refl) 
     ƛ  exts ( σ₁  σ₂)  N
   
sub-sub {M = L · M} = cong₂ _·_ (sub-sub{M = L}) (sub-sub{M = M})

We proceed by induction on the term M.

  • If M = x, then both sides are equal to σ₂ (σ₁ x).

  • If M = ƛ N, we first use the induction hypothesis to show that

      ƛ ⟪ exts σ₂ ⟫ (⟪ exts σ₁ ⟫ N) ≡ ƛ ⟪ exts σ₁ ⨟ exts σ₂ ⟫ N

    and then use the lemma exts-seq to show

      ƛ ⟪ exts σ₁ ⨟ exts σ₂ ⟫ N ≡ ƛ ⟪ exts ( σ₁ ⨟ σ₂) ⟫ N
  • If M is an application, we use the induction hypothesis for both subterms.

The following corollary of sub-sub specializes the first substitution to a renaming.

rename-subst : ∀{Γ Δ Δ′}{M : Γ  }{ρ : Rename Γ Δ}{σ : Subst Δ Δ′}
               σ  (rename ρ M)   σ  ρ  M
rename-subst {Γ}{Δ}{Δ′}{M}{ρ}{σ} =
   begin
      σ  (rename ρ M)
   ≡⟨ cong  σ  (rename-subst-ren{M = M}) 
      σ  ( ren ρ  M)
   ≡⟨ sub-sub{M = M} 
      ren ρ  σ  M
   ≡⟨⟩
      σ  ρ  M
   

Proof of sub-assoc

The proof of sub-assoc follows directly from sub-sub and the definition of sequencing.

sub-assoc : ∀{Γ Δ Σ Ψ : Context} {σ : Subst Γ Δ} {τ : Subst Δ Σ}
             {θ : Subst Σ Ψ}
           ∀{A}  (σ  τ)  θ  (σ  τ  θ) {A}
sub-assoc {Γ}{Δ}{Σ}{Ψ}{σ}{τ}{θ}{A} = extensionality λ x  lemma{x = x}
  where
  lemma :  {x : Γ  A}  ((σ  τ)  θ) x  (σ  τ  θ) x
  lemma {x} =
      begin
        ((σ  τ)  θ) x
      ≡⟨⟩
         θ  ( τ  (σ x))
      ≡⟨ sub-sub{M = σ x} 
         τ  θ  (σ x)
      ≡⟨⟩
        (σ  τ  θ) x
      

Proof of subst-zero-exts-cons

The last equation we needed to prove subst-zero-exts-cons was sub-assoc, so we can now go ahead with its proof. We simply apply the equations for exts and subst-zero and then apply the σ algebra equation to arrive at the normal form M • σ.

subst-zero-exts-cons : ∀{Γ Δ}{σ : Subst Γ Δ}{B}{M : Δ  B}{A}
                      exts σ  subst-zero M  (M  σ) {A}
subst-zero-exts-cons {Γ}{Δ}{σ}{B}{M}{A} =
    begin
      exts σ  subst-zero M
    ≡⟨ cong-seq exts-cons-shift subst-Z-cons-ids 
      (` Z  (σ  ))  (M  ids)
    ≡⟨ sub-dist 
      ( M  ids  (` Z))  ((σ  )  (M  ids))
    ≡⟨ cong-cons (sub-head{σ = ids}) refl 
      M  ((σ  )  (M  ids))
    ≡⟨ cong-cons refl (sub-assoc{σ = σ}) 
      M  (σ  (  (M  ids)))
    ≡⟨ cong-cons refl (cong-seq{σ = σ} refl (sub-tail{M = M}{σ = ids})) 
      M  (σ  ids)
    ≡⟨ cong-cons refl (sub-idR{σ = σ}) 
      M  σ
    

Proof of the substitution lemma

We first prove the generalized form of the substitution lemma, showing that a substitution σ commutes with the substitution of M into N.

⟪ exts σ ⟫ N [ ⟪ σ ⟫ M ] ≡ ⟪ σ ⟫ (N [ M ])

This proof is where the σ algebra pays off. The proof is by direct equational reasoning. Starting with the left-hand side, we apply σ algebra equations, oriented left-to-right, until we arrive at the normal form

⟪ ⟪ σ ⟫ M • σ ⟫ N

We then do the same with the right-hand side, arriving at the same normal form.

subst-commute : ∀{Γ Δ}{N : Γ ,   }{M : Γ  }{σ : Subst Γ Δ }
      exts σ  N [  σ  M ]   σ  (N [ M ])
subst-commute {Γ}{Δ}{N}{M}{σ} =
     begin
        exts σ  N [  σ  M ]
     ≡⟨⟩
        subst-zero ( σ  M)  ( exts σ  N)
     ≡⟨ cong-sub {M =  exts σ  N} subst-Z-cons-ids refl 
         σ  M  ids  ( exts σ  N)
     ≡⟨ sub-sub {M = N} 
        (exts σ)  (( σ  M)  ids)  N
     ≡⟨ cong-sub {M = N} (cong-seq exts-cons-shift refl) refl 
        (` Z  (σ  ))  ( σ  M  ids)  N
     ≡⟨ cong-sub {M = N} (sub-dist {M = ` Z}) refl 
          σ  M  ids  (` Z)  ((σ  )  ( σ  M  ids))  N
     ≡⟨⟩
         σ  M  ((σ  )  ( σ  M  ids))  N
     ≡⟨ cong-sub{M = N} (cong-cons refl (sub-assoc{σ = σ})) refl 
         σ  M  (σ     σ  M  ids)  N
     ≡⟨ cong-sub{M = N} refl refl 
         σ  M  (σ  ids)  N
     ≡⟨ cong-sub{M = N} (cong-cons refl (sub-idR{σ = σ})) refl 
         σ  M  σ  N
     ≡⟨ cong-sub{M = N} (cong-cons refl (sub-idL{σ = σ})) refl 
         σ  M  (ids  σ)  N
     ≡⟨ cong-sub{M = N} (sym sub-dist) refl 
        M  ids  σ  N
     ≡⟨ sym (sub-sub{M = N}) 
        σ  ( M  ids  N)
     ≡⟨ cong  σ  (sym (cong-sub{M = N} subst-Z-cons-ids refl)) 
        σ  (N [ M ])
     

A corollary of subst-commute is that rename also commutes with substitution. In the proof below, we first exchange rename ρ for the substitution ⟪ ren ρ ⟫, and apply subst-commute, and then convert back to rename ρ.

rename-subst-commute : ∀{Γ Δ}{N : Γ ,   }{M : Γ  }{ρ : Rename Γ Δ }
     (rename (ext ρ) N) [ rename ρ M ]  rename ρ (N [ M ])
rename-subst-commute {Γ}{Δ}{N}{M}{ρ} =
     begin
       (rename (ext ρ) N) [ rename ρ M ]
     ≡⟨ cong-sub (cong-sub-zero (rename-subst-ren{M = M}))
                 (rename-subst-ren{M = N}) 
       ( ren (ext ρ)  N) [  ren ρ  M ]
     ≡⟨ cong-sub refl (cong-sub{M = N} ren-ext refl) 
       ( exts (ren ρ)  N) [  ren ρ  M ]
     ≡⟨ subst-commute{N = N} 
       subst (ren ρ) (N [ M ])
     ≡⟨ sym (rename-subst-ren) 
       rename ρ (N [ M ])
     

To present the substitution lemma, we introduce the following notation for substituting a term M for index 1 within term N.

_〔_〕 :  {Γ A B C}
         Γ , B , C  A
         Γ  B
          ---------
         Γ , C  A
_〔_〕 {Γ} {A} {B} {C} N M =
   subst {Γ , B , C} {Γ , C} (exts (subst-zero M)) {A} N

The substitution lemma is stated as follows and proved as a corollary of the subst-commute lemma.

substitution : ∀{Γ}{M : Γ ,  ,   }{N : Γ ,   }{L : Γ  }
     (M [ N ]) [ L ]  (M  L ) [ (N [ L ]) ]
substitution{M = M}{N = N}{L = L} =
   sym (subst-commute{N = M}{M = N}{σ = subst-zero L})

Notes

Most of the properties and proofs in this file are based on the paper Autosubst: Reasoning with de Bruijn Terms and Parallel Substitution by Schafer, Tebbi, and Smolka (ITP 2015). That paper, in turn, is based on the paper of Abadi, Cardelli, Curien, and Levy (1991) that defines the σ algebra.

Unicode

This chapter uses the following unicode:

⟪  U+27EA  MATHEMATICAL LEFT DOUBLE ANGLE BRACKET (\<<)
⟫  U+27EA  MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET (\>>)
↑  U+2191  UPWARDS ARROW (\u)
•  U+2022  BULLET (\bub)
⨟  U+2A1F  Z NOTATION SCHEMA COMPOSITION (C-x 8 RET Z NOTATION SCHEMA COMPOSITION)
〔  U+3014  LEFT TORTOISE SHELL BRACKET (\( option 9 on page 2)
〕  U+3015  RIGHT TORTOISE SHELL BRACKET (\) option 9 on page 2)

后记

Acknowledgements

Thank you to:

  • The inventors of Agda, for a new playground.
  • The authors of Software Foundations, for inspiration.

A special thank you, for inventing ideas on which this book is based, and for hand-holding:

  • Andreas Abel
  • Catarina Coquand
  • Thierry Coquand
  • David Darais
  • Per Martin-Löf
  • Lena Magnusson
  • Conor McBride
  • James McKinna
  • Ulf Norell

$if(contributor)$ For pull requests big and small, and for answering questions on the Agda mailing list:

$for(contributor)$ * $contributor.name$ $endfor$ * [Your name goes here]

$endif$

For contributions to the answers repository:

  • William Cook
  • David Banas

For support:

  • EPSRC Programme Grant EP/K034413/1
  • NSF Grant No. 1814460
  • Foundation Sciences Mathematiques de Paris (FSMP) Distinguised Professor Fellowship

Fonts

module plfa.backmatter.Fonts where

Preferably, all vertical bars should line up.

--------------------------|
abcdefghijklmnopqrstuvwxyz|
ABCDEFGHIJKLMNOPQRSTUVWXYZ|
ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖ ʳˢᵗᵘᵛʷˣʸᶻ|
ᴬᴮ ᴰᴱ ᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾ ᴿ ᵀᵁⱽᵂ   |
ₐ   ₑ  ₕᵢⱼₖₗₘₙₒₚ ᵣₛₜᵤ  ₓ  |
--------------------------|
----------|
0123456789|
⁰¹²³⁴⁵⁶⁷⁸⁹|
₀₁₂₃₄₅₆₇₈₉|
----------|
------------------------|
αβγδεζηθικλμνξοπρστυφχψω|
ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ|
------------------------|
----|
≠≠≠≠|
ηημμ|
ΓΓΔΔ|
ΣΣΠΠ|
λλλλ|
ƛƛƛƛ|
····|
××××|
ℓℓℓℓ|
≡≡≡≡|
¬¬¬¬|
≤≤≥≥|
∅∅∅∅|
————|
††‡‡|
^^^^|
''""|
``~~|
⊎⊎⊃⊃|
∧∧∨∨|
⊗⊗⊗⊗|
⊔⊔⊔⊔|
cᶜbᵇ|
lˡrʳ|
⁻⁻⁺⁺|
ℕℕℕℕ|
∀∀∃∃|
′′″″|
∘∘∘∘|
‌≢≢≃≃|
≲≲≳≳|
≟≟≐≐|
∸∸∸∸|
⟨⟨⟩⟩|
⌊⌊⌋⌋|
⌈⌈⌉⌉|
↑↑↓↓|
⇔⇔↔↔|
→→⇒⇒|
←←⇐⇐|
↞↞↠↠|
∈∈∋∋|
⊢⊢⊣⊣|
⊥⊥⊤⊤|
∷∷∷∷|
∎∎∎∎|
⦂⦂⦂⦂|
∥∥∥∥|
★★★★|
∌∌∉∉|
⨟⨟⨟⨟|
⨆⨆⨆⨆|
〔〔〕〕|
----|

In the book we use the em-dash to make big arrows.

----|
—→—→|
←—←—|
↞—↞—|
—↠—↠|
----|

Here are some characters that are often not monospaced.

----|
😇😇|
😈😈|
⁗⁗|
‴‴|
----|
------------|
------------|
----------|
𝔸𝔹𝔻𝔼𝔽𝔾𝕀𝕁𝕂𝕃𝕄ℕ𝕆𝕊|
𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛|
𝑎𝑏𝑐𝑑𝑒𝑓𝑔𝑖𝑗𝑘|
ℰℱ|
----------|
Alessi, Fabio, Franco Barbanera, and Mariangiola Dezani-Ciancaglini. 2006. “Intersection Types and Lambda Models.” Theoretical Computer Science 355 (2): 108–26.
Barendregt, H. P. 1984. The Lambda Calculus. Vol. 103. Studies in Logic. Elsevier.
Barendregt, Henk, Mario Coppo, and Mariangiola Dezani-Ciancaglini. 1983. “A Filter Lambda Model and the Completeness of Type Assignment.” Journal of Symbolic Logic 48 (4): 931–40. https://doi.org/10.2307/2273659.
Barendregt, Henk, Wil Dekkers, and Richard Statman. 2013. Lambda Calculus with Types. Perspectives in Logic. Cambridge University Press.
Coppo, M., M. Dezani-Ciancaglini, and P. Salle’. 1979. “Functional Characterization of Some Semantic Equalities Inside Lambda-Calculus.” In Automata, Languages and Programming: Sixth Colloquium, edited by Hermann A. Maurer, 133–46. Berlin, Heidelberg: Springer Berlin Heidelberg.
Engeler, Erwin. 1981. “Algebras and Combinators.” Algebra Universalis 13 (1): 389–92.
Nipkow, Tobias. 1996. “More Church-Rosser Proofs (in Isabelle/HOL).” In Proceedings of the 13th International Conference on Automated Deduction: Automated Deduction, 733–47. CADE-13. Berlin, Heidelberg: Springer-Verlag.
Pfenning, Frank. 1992. “A Proof of the Church-Rosser Theorem and Its Representation in a Logical Framework.” CMU-CS-92-186. Carnegie Mellon University; Carnegie Mellon University.
Plotkin, Gordon D. 1972. “A Set-Theoretical Definition of Application.” MIP-R-95. University of Edinburgh.
———. 1993. “Set-Theoretical and Other Elementary Models of the λ-Calculus.” Theoretical Computer Science 121 (1): 351–409.
Ronchi Della Rocca, Simona, and Luca Paolini. 2004. The Parametric Lambda Calculus. Springer.
Schäfer, Steven, Tobias Tebbi, and Gert Smolka. 2015. “Autosubst: Reasoning with de Bruijn Terms and Parallel Substitutions.” In Interactive Theorem Proving: 6th International Conference, ITP 2015, Nanjing, China, August 24-27, 2015, Proceedings 6, 359–74. Springer.
Scott, Dana. 1976. “Data Types as Lattices.” SIAM Journal on Computing 5 (3): 522–87.
Takahashi, M. 1995. “Parallel Reductions in λ-Calculus.” Information and Computation 118 (1): 120–27. https://doi.org/https://doi.org/10.1006/inco.1995.1057.

  1. 此段内容由 Propositions as Types(命题即类型)改编而来, 作者:Philip Wadler,发表于 《ACM 通讯》,2015 年 9 月↩︎